tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is a python API to work with some methods of Tinkoff Open API using REST protocol. It can view history, orders and market information. Also, you can open orders and trades.
If you run this module as CLI program then it realizes simple logic: receiving a lot of options and execute one command. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is a python API to work with some methods of Tinkoff Open API using REST protocol. 6It can view history, orders and market information. Also, you can open orders and trades. 7 8If you run this module as CLI program then it realizes simple logic: receiving a lot of options and execute one command. 9**See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 10 11**Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 12 13About Tinkoff Invest API: https://tinkoff.github.io/investAPI/ 14 15Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/ 16""" 17 18# Copyright (c) 2022 Gilmillin Timur Mansurovich 19# 20# Licensed under the Apache License, Version 2.0 (the "License"); 21# you may not use this file except in compliance with the License. 22# You may obtain a copy of the License at 23# 24# http://www.apache.org/licenses/LICENSE-2.0 25# 26# Unless required by applicable law or agreed to in writing, software 27# distributed under the License is distributed on an "AS IS" BASIS, 28# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29# See the License for the specific language governing permissions and 30# limitations under the License. 31 32 33import sys 34import os 35from argparse import ArgumentParser 36from importlib.metadata import version 37 38from datetime import datetime, timedelta 39from dateutil.tz import tzlocal, tzutc 40from time import sleep 41 42import re 43import json 44import requests 45import traceback as tb 46from typing import Union 47 48from multiprocessing import cpu_count 49from multiprocessing.pool import ThreadPool 50import pandas as pd 51 52from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 53 54from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator 55from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 56 57import UniLogger as uLog # Logger for TKSBrokerAPI 58 59 60# --- Common technical parameters: 61 62PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 63uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 64uLogger.level = 10 # debug level by default for TKSBrokerAPI module 65uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 66 67__version__ = "1.3" # The "major.minor" version setup here, but build number define at the build-server only 68 69CPU_COUNT = cpu_count() # host's real CPU count 70CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 71 72# --- Main constants: 73 74NANO = 0.000000001 # SI-constant nano = 10^-9 75 76 77def NanoToFloat(units: str, nano: int) -> float: 78 """ 79 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 80 81 `NanoToFloat(units="2", nano=500000000) -> 2.5` 82 83 `NanoToFloat(units="0", nano=50000000) -> 0.05` 84 85 :param units: integer string or integer parameter that represents the integer part of number 86 :param nano: integer string or integer parameter that represents the fractional part of number 87 :return: float view of number 88 """ 89 return int(units) + int(nano) * NANO 90 91 92def FloatToNano(number: float) -> dict: 93 """ 94 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 95 96 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 97 98 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 99 100 :param number: float number 101 :return: nano-type view of number: `{"units": "string", "nano": integer}` 102 """ 103 splitByPoint = str(number).split(".") 104 frac = 0 105 106 if len(splitByPoint) > 1: 107 if len(splitByPoint[1]) <= 9: 108 frac = int("{}{}".format( 109 int(splitByPoint[1]), 110 "0" * (9 - len(splitByPoint[1])), 111 )) 112 113 if (number < 0) and (frac > 0): 114 frac = -frac 115 116 return {"units": str(int(number)), "nano": frac} 117 118 119def GetDatesAsString(start: str = None, end: str = None) -> tuple: 120 """ 121 Create tuple of date and time strings with timezone parsed from user-friendly date. 122 123 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 124 125 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 126 An error exception will occur if input date has incorrect format. 127 128 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 129 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 130 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 131 Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago. 132 133 Also, you can use keywords for start if `end=None`: 134 `today` (from 00:00:00 to the end of current day), 135 `yesterday` (-1 day from 00:00:00 to 23:59:59), 136 `week` (-7 day from 00:00:00 to the end of current day), 137 `month` (-30 day from 00:00:00 to the end of current day), 138 `year` (-365 day from 00:00:00 to the end of current day), 139 140 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 141 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 142 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 143 """ 144 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 145 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 146 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 147 148 # time between start and the end of the current day: 149 if start is None or start.lower() == "today": 150 pass 151 152 # from start of the last day to the end of the last day: 153 elif start.lower() == "yesterday": 154 s -= timedelta(days=1) 155 e -= timedelta(days=1) 156 157 # week (-7 day from 00:00:00 to the end of the current day): 158 elif start.lower() == "week": 159 s -= timedelta(days=6) # +1 current day already taken into account 160 161 # month (-30 day from 00:00:00 to the end of current day): 162 elif start.lower() == "month": 163 s -= timedelta(days=29) # +1 current day already taken into account 164 165 # year (-365 day from 00:00:00 to the end of current day): 166 elif start.lower() == "year": 167 s -= timedelta(days=364) # +1 current day already taken into account 168 169 # -N days ago to the end of current day: 170 elif start.startswith('-') and start[1:].isdigit(): 171 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 172 173 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 174 else: 175 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 176 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 177 178 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 179 s = s.strftime(TKS_DATE_TIME_FORMAT) 180 e = e.strftime(TKS_DATE_TIME_FORMAT) 181 182 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 183 184 return s, e 185 186 187class TinkoffBrokerServer: 188 """ 189 This class implements methods to work with Tinkoff broker server. 190 191 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 192 193 About `token`: https://tinkoff.github.io/investAPI/token/ 194 """ 195 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 196 """ 197 Main class init. 198 199 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 200 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 201 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 202 :param useCache: use default cache file with raw data to use instead of `iList`. 203 True by default. Cache is auto-update if new day has come. 204 If you don't want to use cache and always updates raw data then set `useCache=False`. 205 :param defaultCache: path to default cache file. `dump.json` by default. 206 """ 207 if token is None or not token: 208 try: 209 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 210 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 211 212 except KeyError: 213 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 214 raise Exception("Token required") 215 216 else: 217 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 218 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 219 220 if accountId is None or not accountId: 221 try: 222 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 223 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 224 225 except KeyError: 226 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 227 228 else: 229 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 230 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 231 232 self.version = __version__ # duplicate here used TKSBrokerAPI main version 233 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 234 235 Latest version: https://pypi.org/project/tksbrokerapi/ 236 """ 237 238 self.aliases = TKS_TICKER_ALIASES 239 """Some aliases instead official tickers. 240 241 See also: `TKSEnums.TKS_TICKER_ALIASES` 242 """ 243 244 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 245 246 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 247 248 self.ticker = "" 249 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 250 251 See also: `SearchByTicker()`, `SearchInstruments()`. 252 """ 253 254 self.figi = "" 255 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 256 257 See also: `SearchByFIGI()`, `SearchInstruments()`. 258 """ 259 260 self.depth = 1 261 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 262 263 See also: `GetCurrentPrices()`. 264 """ 265 266 self.server = r"https://invest-public-api.tinkoff.ru/rest" 267 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 268 269 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 270 """ 271 272 uLogger.debug("Broker API server: {}".format(self.server)) 273 274 self.timeout = 15 275 """Server operations timeout in seconds. Default: `15`. 276 277 See also: `SendAPIRequest()`. 278 """ 279 280 self.headers = {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {}".format(self.token)} 281 """Headers which send in every request to broker server. Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 282 283 See also: `SendAPIRequest()`. 284 """ 285 286 self.body = None 287 """Request body which send to broker server. Default: `None`. 288 289 See also: `SendAPIRequest()`. 290 """ 291 292 self.historyFile = None 293 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only pandas dataframe. 294 295 See also: `History()`. 296 """ 297 298 self.htmlHistoryFile = "index.html" 299 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 300 301 See also: `ShowHistoryChart()`. 302 """ 303 304 self.instrumentsFile = "instruments.md" 305 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 306 307 See also: `ShowInstrumentsInfo()`. 308 """ 309 310 self.searchResultsFile = "search-results.md" 311 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 312 313 See also: `SearchInstruments()`. 314 """ 315 316 self.pricesFile = "prices.md" 317 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 318 319 See also: `GetListOfPrices()`. 320 """ 321 322 self.infoFile = "info.md" 323 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 324 325 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 326 """ 327 328 self.bondsXLSXFile = "ext-bonds.xlsx" 329 """Filename where wider pandas dataframe with more information about bonds: main info, current prices, 330 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 331 332 See also: `ExtendBondsData()`. 333 """ 334 335 self.calendarFile = "calendar.md" 336 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 337 338 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 339 340 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 341 """ 342 343 self.overviewFile = "overview.md" 344 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 345 346 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 347 """ 348 349 self.overviewDigestFile = "overview-digest.md" 350 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 351 352 See also: `Overview()` with parameter `details="digest"`. 353 """ 354 355 self.overviewPositionsFile = "overview-positions.md" 356 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 357 358 See also: `Overview()` with parameter `details="positions"`. 359 """ 360 361 self.overviewOrdersFile = "overview-orders.md" 362 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 363 364 See also: `Overview()` with parameter `details="orders"`. 365 """ 366 367 self.overviewAnalyticsFile = "overview-analytics.md" 368 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 369 370 See also: `Overview()` with parameter `details="analytics"`. 371 """ 372 373 self.reportFile = "deals.md" 374 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 375 376 See also: `Deals()`. 377 """ 378 379 self.withdrawalLimitsFile = "limits.md" 380 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 381 382 See also: `OverviewLimits()` and `RequestLimits()`. 383 """ 384 385 self.userInfoFile = "user-info.md" 386 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 387 388 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 389 """ 390 391 self.userAccountsFile = "accounts.md" 392 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 393 394 See also: `OverviewAccounts()`, `RequestAccounts()`. 395 """ 396 397 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 398 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 399 400 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 401 402 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 403 """ 404 405 self.iList = None # init iList for raw instruments data 406 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 407 408 See also: `Listing()`, `DumpInstruments()`. 409 """ 410 411 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 412 if useCache: 413 if os.path.exists(self.iListDumpFile): 414 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 415 curTime = datetime.now(tzutc()) 416 417 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 418 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 419 420 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 421 422 else: 423 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 424 425 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 426 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 427 428 else: 429 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 430 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 431 432 else: 433 self.iList = self.Listing() # request new raw instruments data from broker server 434 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 435 436 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 437 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 438 439 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 440 """ 441 442 @staticmethod 443 def _ParseJSON(rawData="{}", debug: bool = False) -> dict: 444 """ 445 Parse JSON from response string. 446 447 :param rawData: this is a string with JSON-formatted text. 448 :param debug: if `True` then print more debug information. 449 :return: JSON (dictionary), parsed from server response string. 450 """ 451 if debug: 452 uLogger.debug("Raw text body:") 453 uLogger.debug(rawData) 454 455 responseJSON = json.loads(rawData) if rawData else {} 456 457 if debug: 458 uLogger.debug("JSON formatted:") 459 for jsonLine in json.dumps(responseJSON, indent=4).split('\n'): 460 uLogger.debug(jsonLine) 461 462 return responseJSON 463 464 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 465 """ 466 Send GET or POST request to broker server and receive JSON object. 467 468 self.header: must be defining with dictionary of headers. 469 self.body: if define then used as request body. None by default. 470 self.timeout: global request timeout, 15 seconds by default. 471 :param url: url with REST request. 472 :param reqType: send "GET" or "POST" request. "GET" by default. 473 :param retry: how many times retry after first request if an 5xx server errors occurred. 474 :param pause: sleep time in seconds between retries. 475 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 476 :return: response JSON (dictionary) from broker. 477 """ 478 if reqType not in ("GET", "POST"): 479 uLogger.error("You can define request type: 'GET' or 'POST'!") 480 raise Exception("Incorrect value") 481 482 if debug: 483 uLogger.debug("Request parameters:") 484 uLogger.debug(" - REST API URL: {}".format(url)) 485 uLogger.debug(" - request type: {}".format(reqType)) 486 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 487 uLogger.debug(" - body: {}".format(self.body)) 488 489 # fast hack to avoid all operations with some tickers/FIGI 490 responseJSON = {} 491 oK = True 492 for item in self.exclude: 493 if item in url: 494 if debug: 495 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 496 497 oK = False 498 break 499 500 if oK: 501 counter = 0 502 response = None 503 errMsg = "" 504 505 while not response and counter <= retry: 506 if reqType == "GET": 507 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 508 509 if reqType == "POST": 510 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 511 512 if debug: 513 uLogger.debug("Response:") 514 uLogger.debug(" - status code: {}".format(response.status_code)) 515 uLogger.debug(" - reason: {}".format(response.reason)) 516 uLogger.debug(" - body length: {}".format(len(response.text))) 517 uLogger.debug(" - headers: {}".format(response.headers)) 518 519 # Server returns some headers: 520 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 521 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 522 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 523 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 524 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 525 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 526 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 527 sleep(rateLimitWait) 528 529 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 530 if 400 <= response.status_code < 500: 531 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 532 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 533 counter = retry + 1 534 535 if 500 <= response.status_code < 600: 536 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 537 uLogger.debug(" - not oK, {}".format(errMsg)) 538 counter += 1 539 540 if counter <= retry: 541 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 542 sleep(pause) 543 544 responseJSON = self._ParseJSON(response.text) 545 546 if errMsg: 547 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 548 uLogger.error(" - not oK, {}".format(errMsg)) 549 550 return responseJSON 551 552 def _IUpdater(self, iType: str) -> tuple: 553 """ 554 Request instrument by type from server. See available API methods for instruments: 555 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 556 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 557 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 558 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 559 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 560 561 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 562 :return: tuple with iType name and list of available instruments of current type for defined user token. 563 """ 564 result = [] 565 566 if iType in TKS_INSTRUMENTS: 567 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 568 569 # all instruments have the same body in API v2 requests: 570 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 571 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 572 result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"] 573 574 return iType, result 575 576 def _IWrapper(self, kwargs): 577 """ 578 Wrapper runs instrument's update method `_IUpdater()`. 579 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 580 """ 581 return self._IUpdater(**kwargs) 582 583 def Listing(self) -> dict: 584 """ 585 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 586 587 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 588 """ 589 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 590 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 591 592 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 593 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 594 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 595 596 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 597 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 598 poolUpdater.close() 599 600 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 601 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 602 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 603 604 # calculate minimum price increment (step) for all instruments and set up instrument's type: 605 for iType in iList.keys(): 606 for ticker in iList[iType]: 607 iList[iType][ticker]["type"] = iType 608 609 if "minPriceIncrement" in iList[iType][ticker].keys(): 610 iList[iType][ticker]["step"] = NanoToFloat( 611 iList[iType][ticker]["minPriceIncrement"]["units"], 612 iList[iType][ticker]["minPriceIncrement"]["nano"], 613 ) 614 615 else: 616 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 617 618 return iList 619 620 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 621 """ 622 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 623 624 See also: `DumpInstruments()`, `Listing()`. 625 626 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 627 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 628 """ 629 if self.iListDumpFile is None or not self.iListDumpFile: 630 uLogger.error("Output name of dump file must be defined!") 631 raise Exception("Filename required") 632 633 if not self.iList or forceUpdate: 634 self.iList = self.Listing() 635 636 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 637 638 # Save as XLSX with separated sheets for every type of instruments: 639 with pd.ExcelWriter( 640 path=xlsxDumpFile, 641 date_format=TKS_DATE_FORMAT, 642 datetime_format=TKS_DATE_TIME_FORMAT, 643 mode="w", 644 ) as writer: 645 for iType in TKS_INSTRUMENTS: 646 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 647 df = df[sorted(df)] # sorted by column names 648 df = df.applymap( 649 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 650 na_action="ignore", 651 ) # converting numbers from nano-type to float in every cell 652 df.to_excel( 653 writer, 654 sheet_name=iType, 655 encoding="UTF-8", 656 freeze_panes=(1, 1), 657 ) # saving as XLSX-file with freeze first row and column as headers 658 659 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 660 661 def DumpInstruments(self, forceUpdate: bool = True) -> str: 662 """ 663 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 664 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 665 666 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 667 668 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 669 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 670 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 671 """ 672 if self.iListDumpFile is None or not self.iListDumpFile: 673 uLogger.error("Output name of dump file must be defined!") 674 raise Exception("Filename required") 675 676 if not self.iList or forceUpdate: 677 self.iList = self.Listing() 678 679 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 680 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 681 fH.write(jsonDump) 682 683 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 684 685 return jsonDump 686 687 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 688 """ 689 Show information about one instrument defined by json data and prints it in Markdown format. 690 691 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 692 693 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 694 :param show: if `True` then also printing information about instrument and its current price. 695 :return: multilines text in Markdown format with information about one instrument. 696 """ 697 splitLine = "| | |\n" 698 infoText = "" 699 700 if iJSON is not None and iJSON and isinstance(iJSON, dict): 701 info = [ 702 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 703 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 704 "| Parameters | Values |\n", 705 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 706 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 707 "| Full name: | {:<54} |\n".format(iJSON["name"]), 708 ] 709 710 if "sector" in iJSON.keys() and iJSON["sector"]: 711 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 712 713 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 714 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 715 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 716 ))) 717 718 info.extend([ 719 splitLine, 720 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 721 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 722 ]) 723 724 if "isin" in iJSON.keys() and iJSON["isin"]: 725 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 726 727 if "classCode" in iJSON.keys(): 728 info.append("| Class Code: | {:<54} |\n".format(iJSON["classCode"])) 729 730 info.extend([ 731 splitLine, 732 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 733 splitLine, 734 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 735 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 736 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 737 ]) 738 739 if iJSON["figi"]: 740 self.figi = iJSON["figi"] 741 iJSON = iJSON | self.RequestTradingStatus() 742 743 info.extend([ 744 splitLine, 745 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 746 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 747 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 748 ]) 749 750 info.append(splitLine) 751 752 if "type" in iJSON.keys() and iJSON["type"]: 753 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 754 755 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 756 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 757 758 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 759 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 760 761 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 762 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 763 764 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 765 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 766 767 if "focusType" in iJSON.keys() and iJSON["focusType"]: 768 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 769 770 if "assetType" in iJSON.keys() and iJSON["assetType"]: 771 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 772 773 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 774 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 775 776 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 777 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 778 779 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 780 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 781 782 if "currency" in iJSON.keys(): 783 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 784 785 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 786 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 787 788 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 789 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 790 791 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 792 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 793 794 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 795 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 796 797 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 798 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 799 800 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 801 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 802 803 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 804 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 805 806 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 807 info.append("| Perpetual bond: | Yes |\n") 808 809 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 810 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 811 812 iExt = None 813 if iJSON["type"] == "Bonds": 814 info.extend([ 815 splitLine, 816 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 817 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 818 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 819 iJSON["nominal"]["currency"], 820 )), 821 ]) 822 823 if "floatingCouponFlag" in iJSON.keys(): 824 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 825 826 if "amortizationFlag" in iJSON.keys(): 827 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 828 829 info.append(splitLine) 830 831 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 832 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 833 834 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 835 836 info.extend([ 837 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 838 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 839 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 840 ]) 841 842 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 843 info.append("| Current Accrued Interest (ACI): | {:<54} |\n".format("{:.2f} {}".format( 844 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 845 iJSON["aciValue"]["currency"] 846 ))) 847 848 if "currentPrice" in iJSON.keys(): 849 info.append(splitLine) 850 851 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 852 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 853 854 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 855 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 856 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 857 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 858 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 859 860 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 861 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 862 863 info.extend([ 864 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 865 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 866 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 867 )), 868 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 869 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 870 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 871 )), 872 "| Changes between last deal price and last close | {:<54} |\n".format( 873 "{:.2f}%{}".format( 874 iJSON["currentPrice"]["changes"], 875 " ({}{:.2f} {})".format( 876 "+" if bondChangesDelta > 0 else "", 877 bondChangesDelta, 878 aciCurrency 879 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 880 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 881 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 882 currency 883 ), 884 ) 885 ), 886 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 887 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 888 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 889 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 890 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 891 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 892 )), 893 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 894 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 895 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 896 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 897 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 898 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 899 )), 900 ]) 901 902 if "lot" in iJSON.keys(): 903 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 904 905 if "step" in iJSON.keys() and iJSON["step"] != 0: 906 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 907 908 # Add bond payment calendar: 909 if iJSON["type"] == "Bonds": 910 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 911 info.extend(["\n", strCalendar]) 912 913 infoText += "".join(info) 914 915 if show: 916 uLogger.info("{}".format(infoText)) 917 918 else: 919 uLogger.debug("{}".format(infoText)) 920 921 if self.infoFile is not None: 922 with open(self.infoFile, "w", encoding="UTF-8") as fH: 923 fH.write(infoText) 924 925 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 926 927 return infoText 928 929 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 930 """ 931 Search and return raw broker's information about instrument by its ticker. 932 `ticker` must be defined! If debug=True then print all debug messages. 933 934 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 935 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 936 :param debug: if `True` then print all debug console messages. 937 :return: JSON formatted data with information about instrument. 938 """ 939 tickerJSON = {} 940 if debug: 941 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 942 943 if not self.ticker: 944 uLogger.warning("self.ticker variable is not be empty!") 945 946 else: 947 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 948 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 949 raise Exception("Instrument not allowed") 950 951 if not self.iList: 952 self.iList = self.Listing() 953 954 if self.ticker in self.iList["Shares"].keys(): 955 tickerJSON = self.iList["Shares"][self.ticker] 956 if debug: 957 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 958 959 elif self.ticker in self.iList["Currencies"].keys(): 960 tickerJSON = self.iList["Currencies"][self.ticker] 961 if debug: 962 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 963 964 elif self.ticker in self.iList["Bonds"].keys(): 965 tickerJSON = self.iList["Bonds"][self.ticker] 966 if debug: 967 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 968 969 elif self.ticker in self.iList["Etfs"].keys(): 970 tickerJSON = self.iList["Etfs"][self.ticker] 971 if debug: 972 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 973 974 elif self.ticker in self.iList["Futures"].keys(): 975 tickerJSON = self.iList["Futures"][self.ticker] 976 if debug: 977 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 978 979 if tickerJSON: 980 self.figi = tickerJSON["figi"] 981 982 if requestPrice: 983 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 984 985 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 986 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 987 988 else: 989 tickerJSON["currentPrice"]["changes"] = 0 990 991 if show: 992 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 993 994 else: 995 if show: 996 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 997 998 return tickerJSON 999 1000 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1001 """ 1002 Search and return raw broker's information about instrument by its FIGI. 1003 `figi` must be defined! If debug=True then print all debug messages. 1004 1005 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1006 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1007 :param debug: if `True` then print all debug console messages. 1008 :return: JSON formatted data with information about instrument. 1009 """ 1010 figiJSON = {} 1011 if debug: 1012 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1013 1014 if not self.figi: 1015 uLogger.warning("self.figi variable is not be empty!") 1016 1017 else: 1018 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1019 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1020 raise Exception("Instrument not allowed") 1021 1022 if not self.iList: 1023 self.iList = self.Listing() 1024 1025 for item in self.iList["Shares"].keys(): 1026 if self.figi == self.iList["Shares"][item]["figi"]: 1027 figiJSON = self.iList["Shares"][item] 1028 1029 if debug: 1030 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1031 1032 break 1033 1034 if not figiJSON: 1035 for item in self.iList["Currencies"].keys(): 1036 if self.figi == self.iList["Currencies"][item]["figi"]: 1037 figiJSON = self.iList["Currencies"][item] 1038 1039 if debug: 1040 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1041 1042 break 1043 1044 if not figiJSON: 1045 for item in self.iList["Bonds"].keys(): 1046 if self.figi == self.iList["Bonds"][item]["figi"]: 1047 figiJSON = self.iList["Bonds"][item] 1048 1049 if debug: 1050 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1051 1052 break 1053 1054 if not figiJSON: 1055 for item in self.iList["Etfs"].keys(): 1056 if self.figi == self.iList["Etfs"][item]["figi"]: 1057 figiJSON = self.iList["Etfs"][item] 1058 1059 if debug: 1060 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1061 1062 break 1063 1064 if not figiJSON: 1065 for item in self.iList["Futures"].keys(): 1066 if self.figi == self.iList["Futures"][item]["figi"]: 1067 figiJSON = self.iList["Futures"][item] 1068 1069 if debug: 1070 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1071 1072 break 1073 1074 if figiJSON: 1075 self.figi = figiJSON["figi"] 1076 self.ticker = figiJSON["ticker"] 1077 1078 if requestPrice: 1079 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1080 1081 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1082 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1083 1084 else: 1085 figiJSON["currentPrice"]["changes"] = 0 1086 1087 if show: 1088 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1089 1090 else: 1091 if show: 1092 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1093 1094 return figiJSON 1095 1096 def GetCurrentPrices(self, show: bool = True) -> dict: 1097 """ 1098 Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record: 1099 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1100 1101 See also: 1102 1103 :param show: if `True` then print DOM to log and console. 1104 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1105 """ 1106 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1107 1108 if self.depth < 1: 1109 uLogger.error("Depth of Market (DOM) must be >=1!") 1110 raise Exception("Incorrect value") 1111 1112 if not (self.ticker or self.figi): 1113 uLogger.error("self.ticker or self.figi variables must be defined!") 1114 raise Exception("Ticker or FIGI required") 1115 1116 if self.ticker and not self.figi: 1117 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1118 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1119 1120 if not self.ticker and self.figi: 1121 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1122 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1123 1124 if not self.figi: 1125 uLogger.error("FIGI is not defined!") 1126 raise Exception("Ticker or FIGI required") 1127 1128 else: 1129 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1130 1131 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1132 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1133 self.body = str({"figi": self.figi, "depth": self.depth}) 1134 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") 1135 1136 if pricesResponse: 1137 # list of dicts with sellers orders: 1138 prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1139 1140 # list of dicts with buyers orders: 1141 prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1142 1143 # max price of instrument at this time: 1144 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1145 1146 # min price of instrument at this time: 1147 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1148 1149 # last price of deal with instrument: 1150 prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0 1151 1152 # last close price of instrument: 1153 prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0 1154 1155 else: 1156 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1157 uLogger.debug("Server response: {}".format(pricesResponse)) 1158 1159 if show: 1160 if prices["buy"] or prices["sell"]: 1161 info = [ 1162 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1163 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1164 self.ticker, 1165 self.figi, 1166 self.depth, 1167 ), 1168 uLog.sepShort, "\n", 1169 " Orders of Buyers | Orders of Sellers\n", 1170 uLog.sepShort, "\n", 1171 " Sell prices (vol.) | Buy prices (vol.)\n", 1172 uLog.sepShort, "\n", 1173 ] 1174 1175 if not prices["buy"]: 1176 info.append(" | No orders!\n") 1177 sumBuy = 0 1178 1179 else: 1180 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1181 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1182 for item in maxMinSorted: 1183 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1184 1185 if not prices["sell"]: 1186 info.append("No orders! |\n") 1187 sumSell = 0 1188 1189 else: 1190 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1191 for item in prices["sell"]: 1192 info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1193 1194 info.extend([ 1195 uLog.sepShort, "\n", 1196 "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1197 uLog.sepShort, "\n", 1198 ]) 1199 1200 infoText = "".join(info) 1201 1202 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1203 1204 else: 1205 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1206 1207 return prices 1208 1209 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1210 """ 1211 This method get and show information about all available broker instruments for current user account. 1212 If `instrumentsFile` string is not empty then also save information to this file. 1213 1214 :param show: if `True` then print results to console, if `False` - print only to file. 1215 :return: multi-lines string with all available broker instruments 1216 """ 1217 if not self.iList: 1218 self.iList = self.Listing() 1219 1220 info = [ 1221 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1222 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1223 ] 1224 1225 # add instruments count by type: 1226 for iType in self.iList.keys(): 1227 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1228 1229 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1230 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1231 1232 # generating info tables with all instruments by type: 1233 for iType in self.iList.keys(): 1234 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1235 1236 for instrument in self.iList[iType].keys(): 1237 iName = self.iList[iType][instrument]["name"] # instrument's name 1238 if len(iName) > 57: 1239 iName = "{}...".format(iName[:54]) # right trim for a long string 1240 1241 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1242 self.iList[iType][instrument]["ticker"], 1243 iName, 1244 self.iList[iType][instrument]["figi"], 1245 self.iList[iType][instrument]["currency"], 1246 self.iList[iType][instrument]["lot"], 1247 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1248 )) 1249 1250 infoText = "".join(info) 1251 1252 if show: 1253 uLogger.info(infoText) 1254 1255 if self.instrumentsFile: 1256 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1257 fH.write(infoText) 1258 1259 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1260 1261 return infoText 1262 1263 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1264 """ 1265 This method search and show information about instruments by part of its ticker, FIGI or name. 1266 If `searchResultsFile` string is not empty then also save information to this file. 1267 1268 :param pattern: string with part of ticker, FIGI or instrument's name. 1269 :param show: if `True` then print results to console, if `False` - return list of result only. 1270 :return: list of dictionaries with all found instruments. 1271 """ 1272 if not self.iList: 1273 self.iList = self.Listing() 1274 1275 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1276 compiledPattern = re.compile(pattern, re.IGNORECASE) 1277 1278 for iType in self.iList: 1279 for instrument in self.iList[iType].values(): 1280 searchResult = compiledPattern.search(" ".join( 1281 [instrument["ticker"], instrument["figi"], instrument["name"]] 1282 )) 1283 1284 if searchResult: 1285 searchResults[iType][instrument["ticker"]] = instrument 1286 1287 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1288 info = [ 1289 "# Search results\n\n", 1290 "* **Search pattern:** [{}]\n".format(pattern), 1291 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1292 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1293 ] 1294 infoShort = info[:] 1295 1296 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1297 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1298 skippedLine = "| ... | ... | ... | ... |\n" 1299 1300 if resultsLen == 0: 1301 info.append("\nNo results\n") 1302 infoShort.append("\nNo results\n") 1303 uLogger.warning("No results. Try changing your search pattern.") 1304 1305 else: 1306 for iType in searchResults: 1307 iTypeValuesCount = len(searchResults[iType].values()) 1308 if iTypeValuesCount > 0: 1309 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1310 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1311 1312 for instrument in searchResults[iType].values(): 1313 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1314 instrument["type"], 1315 instrument["ticker"], 1316 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1317 instrument["figi"], 1318 )) 1319 1320 if iTypeValuesCount <= 5: 1321 infoShort.extend(info[-iTypeValuesCount:]) 1322 1323 else: 1324 infoShort.extend(info[-5:]) 1325 infoShort.append(skippedLine) 1326 1327 infoText = "".join(info) 1328 infoTextShort = "".join(infoShort) 1329 1330 if show: 1331 uLogger.info(infoTextShort) 1332 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1333 1334 if self.searchResultsFile: 1335 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1336 fH.write(infoText) 1337 1338 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1339 1340 return searchResults 1341 1342 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1343 """ 1344 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1345 1346 :param instruments: list of strings with tickers or FIGIs. 1347 :return: list with unique instrument FIGIs only. 1348 """ 1349 requestedInstruments = [] 1350 for iName in instruments: 1351 if iName not in self.aliases.keys(): 1352 if iName not in requestedInstruments: 1353 requestedInstruments.append(iName) 1354 1355 else: 1356 if iName not in requestedInstruments: 1357 if self.aliases[iName] not in requestedInstruments: 1358 requestedInstruments.append(self.aliases[iName]) 1359 1360 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1361 1362 onlyUniqueFIGIs = [] 1363 for iName in requestedInstruments: 1364 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1365 continue 1366 1367 self.ticker = iName 1368 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1369 1370 if not iData: 1371 self.ticker = "" 1372 self.figi = iName 1373 1374 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1375 1376 if not iData: 1377 self.figi = "" 1378 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1379 1380 if iData and iData["figi"] not in onlyUniqueFIGIs: 1381 onlyUniqueFIGIs.append(iData["figi"]) 1382 1383 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1384 1385 return onlyUniqueFIGIs 1386 1387 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1388 """ 1389 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1390 See limits: https://tinkoff.github.io/investAPI/limits/ 1391 If `pricesFile` string is not empty then also save information to this file. 1392 1393 :param instruments: list of strings with tickers or FIGIs. 1394 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1395 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1396 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1397 """ 1398 if instruments is None or not instruments: 1399 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1400 raise Exception("Ticker or FIGI required") 1401 1402 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1403 1404 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1405 1406 iList = [] # trying to get info and current prices about all unique instruments: 1407 for self.figi in onlyUniqueFIGIs: 1408 iData = self.SearchByFIGI(requestPrice=True) 1409 iList.append(iData) 1410 1411 self.ShowListOfPrices(iList, show) 1412 1413 return iList 1414 1415 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1416 """ 1417 Show table contains current prices of given instruments. 1418 1419 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1420 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1421 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1422 :return: multilines text in Markdown format as a table contains current prices. 1423 """ 1424 infoText = "" 1425 1426 if show or self.pricesFile: 1427 info = [ 1428 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1429 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1430 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1431 ] 1432 1433 for item in iList: 1434 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1435 item["ticker"], 1436 item["figi"], 1437 item["type"], 1438 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1439 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1440 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1441 "{} / {}".format( 1442 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1443 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1444 ), 1445 "{} / {}".format( 1446 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1447 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1448 ), 1449 item["currency"], 1450 )) 1451 1452 infoText = "".join(info) 1453 1454 if show: 1455 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1456 1457 if self.pricesFile: 1458 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1459 fH.write(infoText) 1460 1461 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1462 1463 return infoText 1464 1465 def RequestTradingStatus(self) -> dict: 1466 """ 1467 Requesting trading status for the instrument defined by `figi` variable. 1468 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1469 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1470 1471 :return: dictionary with trading status attributes. Response example: 1472 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1473 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1474 """ 1475 if self.figi is None or not self.figi: 1476 uLogger.error("Variable `figi` must be defined for using this method!") 1477 raise Exception("FIGI required") 1478 1479 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1480 1481 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1482 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1483 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1484 1485 uLogger.debug("Records about current trading status successfully received") 1486 1487 return tradingStatus 1488 1489 def RequestPortfolio(self) -> dict: 1490 """ 1491 Requesting actual user's portfolio for current `accountId`. 1492 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1493 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1494 1495 :return: dictionary with user's portfolio. 1496 """ 1497 if self.accountId is None or not self.accountId: 1498 uLogger.error("Variable `accountId` must be defined for using this method!") 1499 raise Exception("Account ID required") 1500 1501 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1502 1503 self.body = str({"accountId": self.accountId}) 1504 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1505 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1506 1507 uLogger.debug("Records about user's portfolio successfully received") 1508 1509 return rawPortfolio 1510 1511 def RequestPositions(self) -> dict: 1512 """ 1513 Requesting open positions by currencies and instruments for current `accountId`. 1514 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1515 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1516 1517 :return: dictionary with open positions by instruments. 1518 """ 1519 if self.accountId is None or not self.accountId: 1520 uLogger.error("Variable `accountId` must be defined for using this method!") 1521 raise Exception("Account ID required") 1522 1523 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1524 1525 self.body = str({"accountId": self.accountId}) 1526 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1527 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1528 1529 uLogger.debug("Records about current open positions successfully received") 1530 1531 return rawPositions 1532 1533 def RequestPendingOrders(self) -> list: 1534 """ 1535 Requesting current actual pending orders for current `accountId`. 1536 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1537 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1538 1539 :return: list of dictionaries with pending orders. 1540 """ 1541 if self.accountId is None or not self.accountId: 1542 uLogger.error("Variable `accountId` must be defined for using this method!") 1543 raise Exception("Account ID required") 1544 1545 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1546 1547 self.body = str({"accountId": self.accountId}) 1548 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1549 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1550 1551 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1552 1553 return rawOrders 1554 1555 def RequestStopOrders(self) -> list: 1556 """ 1557 Requesting current actual stop orders for current `accountId`. 1558 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1559 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1560 1561 :return: list of dictionaries with stop orders. 1562 """ 1563 if self.accountId is None or not self.accountId: 1564 uLogger.error("Variable `accountId` must be defined for using this method!") 1565 raise Exception("Account ID required") 1566 1567 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1568 1569 self.body = str({"accountId": self.accountId}) 1570 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1571 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1572 1573 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1574 1575 return rawStopOrders 1576 1577 def Overview(self, show: bool = False, details: str = "full") -> dict: 1578 """ 1579 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1580 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1581 are defined then also save information to file. 1582 1583 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1584 many requests about the state of the portfolio, and then, based on the received data, a large number 1585 of calculation and statistics are collected. 1586 1587 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1588 :param details: how detailed should the information be? You should specify one of strings: 1589 `full` - shows full available information about portfolio status (by default), 1590 `positions` - shows only open positions, 1591 `digest` - show a short digest of the portfolio status, 1592 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1593 `orders` - shows only sections of open limits and stop orders. 1594 :return: dictionary with client's raw portfolio and some statistics. 1595 """ 1596 if self.accountId is None or not self.accountId: 1597 uLogger.error("Variable `accountId` must be defined for using this method!") 1598 raise Exception("Account ID required") 1599 1600 view = { 1601 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1602 "headers": {}, # list of dictionaries, response headers without "positions" section 1603 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1604 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1605 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1606 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1607 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1608 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1609 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1610 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1611 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1612 }, 1613 "stat": { # --- some statistics calculated using "raw" sections: 1614 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1615 "availableRUB": 0., # available rubles (without other currencies) 1616 "blockedRUB": 0., # blocked sum in Russian Rouble 1617 "totalChangesRUB": 0., # changes for all open trades in RUB 1618 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1619 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1620 "sharesCostRUB": 0., # costs of all shares in RUB 1621 "bondsCostRUB": 0., # costs of all bonds in RUB 1622 "etfsCostRUB": 0., # costs of all etfs in RUB 1623 "futuresCostRUB": 0., # costs of all futures in RUB 1624 "Currencies": [], # list of dictionaries of all currencies statistics 1625 "Shares": [], # list of dictionaries of all shares statistics 1626 "Bonds": [], # list of dictionaries of all bonds statistics 1627 "Etfs": [], # list of dictionaries of all etfs statistics 1628 "Futures": [], # list of dictionaries of all futures statistics 1629 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1630 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1631 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1632 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1633 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1634 }, 1635 "analytics": { # --- some analytics of portfolio: 1636 "distrByAssets": {}, # portfolio distribution by assets 1637 "distrByCompanies": {}, # portfolio distribution by companies 1638 "distrBySectors": {}, # portfolio distribution by sectors 1639 "distrByCurrencies": {}, # portfolio distribution by currencies 1640 "distrByCountries": {}, # portfolio distribution by countries 1641 } 1642 } 1643 1644 details = details.lower() 1645 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1646 if details not in availableDetails: 1647 details = "full" 1648 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1649 1650 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1651 1652 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1653 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1654 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1655 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1656 1657 # save response headers without "positions" section: 1658 for key in portfolioResponse.keys(): 1659 if key != "positions": 1660 view["raw"]["headers"][key] = portfolioResponse[key] 1661 1662 else: 1663 continue 1664 1665 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1666 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1667 for item in portfolioResponse["positions"]: 1668 if item["instrumentType"] == "currency": 1669 self.figi = item["figi"] 1670 curr = self.SearchByFIGI(requestPrice=False) 1671 1672 # current price of currency in RUB: 1673 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1674 "name": curr["name"], 1675 "currentPrice": NanoToFloat( 1676 item["currentPrice"]["units"], 1677 item["currentPrice"]["nano"] 1678 ), 1679 } 1680 1681 view["raw"]["Currencies"].append(item) 1682 1683 elif item["instrumentType"] == "share": 1684 view["raw"]["Shares"].append(item) 1685 1686 elif item["instrumentType"] == "bond": 1687 view["raw"]["Bonds"].append(item) 1688 1689 elif item["instrumentType"] == "etf": 1690 view["raw"]["Etfs"].append(item) 1691 1692 elif item["instrumentType"] == "futures": 1693 view["raw"]["Futures"].append(item) 1694 1695 else: 1696 continue 1697 1698 # how many volume of currencies (by ISO currency name) are blocked: 1699 for item in view["raw"]["positions"]["blocked"]: 1700 blocked = NanoToFloat(item["units"], item["nano"]) 1701 if blocked > 0: 1702 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1703 1704 # how many volume of instruments (by FIGI) are blocked: 1705 for item in view["raw"]["positions"]["securities"]: 1706 blocked = int(item["blocked"]) 1707 if blocked > 0: 1708 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1709 1710 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1711 1712 if "rub" in allBlocked.keys(): 1713 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1714 1715 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1716 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1717 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1718 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1719 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1720 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1721 view["stat"]["portfolioCostRUB"] = sum([ 1722 view["stat"]["allCurrenciesCostRUB"], 1723 view["stat"]["sharesCostRUB"], 1724 view["stat"]["bondsCostRUB"], 1725 view["stat"]["etfsCostRUB"], 1726 view["stat"]["futuresCostRUB"], 1727 ]) 1728 1729 # --- calculating some portfolio statistics: 1730 byComp = {} # distribution by companies 1731 bySect = {} # distribution by sectors 1732 byCurr = {} # distribution by currencies (include RUB) 1733 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1734 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1735 1736 for item in portfolioResponse["positions"]: 1737 self.figi = item["figi"] 1738 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1739 1740 if instrument: 1741 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1742 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1743 1744 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1745 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1746 1747 else: 1748 blocked = 0 1749 1750 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1751 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1752 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1753 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1754 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1755 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1756 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1757 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1758 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1759 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1760 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1761 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1762 1763 statData = { 1764 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1765 "ticker": instrument["ticker"], # ticker by FIGI 1766 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1767 "volume": volume, # available volume of instrument 1768 "lots": lots, # volume in lots of instrument 1769 "direction": direction, # direction of an instrument's position: short or long 1770 "blocked": blocked, # blocked volume of currency or instrument 1771 "currentPrice": curPrice, # current instrument's price in basic asset 1772 "average": average, # current average position price 1773 "cost": cost, # current cost of all volume of instrument in basic asset 1774 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1775 "costRUB": costRUB, # cost of instrument in ruble 1776 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1777 "profit": profit, # expected profit at current moment 1778 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1779 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1780 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1781 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1782 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1783 "step": instrument["step"], # minimum price increment 1784 } 1785 1786 # adding distribution by unique countries: 1787 if statData["country"] not in byCountry.keys(): 1788 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1789 1790 else: 1791 byCountry[statData["country"]]["cost"] += costRUB 1792 byCountry[statData["country"]]["percent"] += percentCostRUB 1793 1794 if item["instrumentType"] != "currency": 1795 # adding distribution by unique companies: 1796 if statData["name"]: 1797 if statData["name"] not in byComp.keys(): 1798 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1799 1800 else: 1801 byComp[statData["name"]]["cost"] += costRUB 1802 byComp[statData["name"]]["percent"] += percentCostRUB 1803 1804 # adding distribution by unique sectors: 1805 if statData["sector"] not in bySect.keys(): 1806 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1807 1808 else: 1809 bySect[statData["sector"]]["cost"] += costRUB 1810 bySect[statData["sector"]]["percent"] += percentCostRUB 1811 1812 # adding distribution by unique currencies: 1813 if currency not in byCurr.keys(): 1814 byCurr[currency] = { 1815 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1816 "cost": costRUB, 1817 "percent": percentCostRUB 1818 } 1819 1820 else: 1821 byCurr[currency]["cost"] += costRUB 1822 byCurr[currency]["percent"] += percentCostRUB 1823 1824 # saving statistics for every instrument: 1825 if item["instrumentType"] == "currency": 1826 view["stat"]["Currencies"].append(statData) 1827 1828 # update dict with free funds for trading (total - blocked) by currencies 1829 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1830 view["stat"]["funds"][currency] = { 1831 "total": volume, 1832 "totalCostRUB": costRUB, # total volume cost in rubles 1833 "free": volume - blocked, 1834 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1835 } 1836 1837 elif item["instrumentType"] == "share": 1838 view["stat"]["Shares"].append(statData) 1839 1840 elif item["instrumentType"] == "bond": 1841 view["stat"]["Bonds"].append(statData) 1842 1843 elif item["instrumentType"] == "etf": 1844 view["stat"]["Etfs"].append(statData) 1845 1846 elif item["instrumentType"] == "Futures": 1847 view["stat"]["Futures"].append(statData) 1848 1849 else: 1850 continue 1851 1852 # total changes in Russian Ruble: 1853 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1854 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1855 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1856 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1857 view["stat"]["funds"]["rub"] = { 1858 "total": view["stat"]["availableRUB"], 1859 "totalCostRUB": view["stat"]["availableRUB"], 1860 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1861 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1862 } 1863 1864 # --- pending orders sector data: 1865 uniquePendingOrders = [] 1866 uniquePendingOrdersFIGIs = [] 1867 for item in view["raw"]["orders"]: 1868 if item["figi"] not in uniquePendingOrdersFIGIs: 1869 uniquePendingOrdersFIGIs.append(item["figi"]) 1870 uniquePendingOrders.append(item) 1871 1872 for item in uniquePendingOrders: 1873 self.figi = item["figi"] 1874 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1875 1876 if instrument: 1877 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1878 orderType = TKS_ORDER_TYPES[item["orderType"]] 1879 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1880 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1881 1882 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1883 if item["direction"] == "ORDER_DIRECTION_BUY": 1884 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1885 1886 else: 1887 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1888 1889 # requested price for order execution: 1890 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1891 1892 # necessary changes in percent to reach target from current price: 1893 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1894 1895 view["stat"]["orders"].append({ 1896 "orderID": item["orderId"], # orderId number parameter of current order 1897 "figi": item["figi"], # FIGI identification 1898 "ticker": instrument["ticker"], # ticker name by FIGI 1899 "lotsRequested": item["lotsRequested"], # requested lots value 1900 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1901 "currentPrice": lastPrice, # current instrument's price for defined action 1902 "targetPrice": target, # requested price for order execution in base currency 1903 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1904 "percentChanges": changes, # changes in percent to target from current price 1905 "currency": item["currency"], # instrument's currency name 1906 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1907 "type": orderType, # type of order from TKS_ORDER_TYPES 1908 "status": orderState, # order status from TKS_ORDER_STATES 1909 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1910 }) 1911 1912 # --- stop orders sector data: 1913 uniqueStopOrders = [] 1914 uniqueStopOrdersFIGIs = [] 1915 for item in view["raw"]["stopOrders"]: 1916 if item["figi"] not in uniqueStopOrdersFIGIs: 1917 uniqueStopOrdersFIGIs.append(item["figi"]) 1918 uniqueStopOrders.append(item) 1919 1920 for item in uniqueStopOrders: 1921 self.figi = item["figi"] 1922 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1923 1924 if instrument: 1925 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1926 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1927 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1928 1929 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1930 if "expirationTime" in item.keys(): 1931 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1932 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1933 1934 else: 1935 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1936 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1937 1938 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1939 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1940 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1941 1942 else: 1943 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1944 1945 # requested price when stop-order executed: 1946 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1947 1948 # price for limit-order, set up when stop-order executed: 1949 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1950 1951 # necessary changes in percent to reach target from current price: 1952 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1953 1954 view["stat"]["stopOrders"].append({ 1955 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1956 "figi": item["figi"], # FIGI identification 1957 "ticker": instrument["ticker"], # ticker name by FIGI 1958 "lotsRequested": item["lotsRequested"], # requested lots value 1959 "currentPrice": lastPrice, # current instrument's price for defined action 1960 "targetPrice": target, # requested price for stop-order execution in base currency 1961 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1962 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1963 "percentChanges": changes, # changes in percent to target from current price 1964 "currency": item["currency"], # instrument's currency name 1965 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1966 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1967 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1968 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1969 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1970 }) 1971 1972 # --- calculating data for analytics section: 1973 # portfolio distribution by assets: 1974 view["analytics"]["distrByAssets"] = { 1975 "Ruble": { 1976 "uniques": 1, 1977 "cost": view["stat"]["availableRUB"], 1978 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1979 }, 1980 "Currencies": { 1981 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1982 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1983 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1984 }, 1985 "Shares": { 1986 "uniques": len(view["stat"]["Shares"]), 1987 "cost": view["stat"]["sharesCostRUB"], 1988 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1989 }, 1990 "Bonds": { 1991 "uniques": len(view["stat"]["Bonds"]), 1992 "cost": view["stat"]["bondsCostRUB"], 1993 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1994 }, 1995 "Etfs": { 1996 "uniques": len(view["stat"]["Etfs"]), 1997 "cost": view["stat"]["etfsCostRUB"], 1998 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1999 }, 2000 "Futures": { 2001 "uniques": len(view["stat"]["Futures"]), 2002 "cost": view["stat"]["futuresCostRUB"], 2003 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2004 }, 2005 } 2006 2007 # portfolio distribution by companies: 2008 view["analytics"]["distrByCompanies"]["All money cash"] = { 2009 "ticker": "", 2010 "cost": view["stat"]["allCurrenciesCostRUB"], 2011 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2012 } 2013 view["analytics"]["distrByCompanies"].update(byComp) 2014 2015 # portfolio distribution by sectors: 2016 view["analytics"]["distrBySectors"]["All money cash"] = { 2017 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2018 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2019 } 2020 view["analytics"]["distrBySectors"].update(bySect) 2021 2022 # portfolio distribution by currencies: 2023 view["analytics"]["distrByCurrencies"].update(byCurr) 2024 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2025 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2026 2027 # portfolio distribution by countries: 2028 view["analytics"]["distrByCountries"].update(byCountry) 2029 2030 # --- Prepare text statistics overview in human-readable: 2031 if show: 2032 # Whatever the value `details`, header not changes: 2033 info = [ 2034 "# Client's portfolio\n\n", 2035 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2036 "* **Account ID:** [{}]\n".format(self.accountId), 2037 ] 2038 2039 if details in ["full", "positions", "digest"]: 2040 info.extend([ 2041 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2042 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2043 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2044 view["stat"]["totalChangesRUB"], 2045 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2046 view["stat"]["totalChangesPercentRUB"], 2047 ), 2048 ]) 2049 2050 if details in ["full", "positions"]: 2051 info.extend([ 2052 "## Open positions\n\n", 2053 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2054 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2055 "| Ruble | {:>31} | | | | | |\n".format( 2056 "{:.2f} ({:.2f}) rub".format( 2057 view["stat"]["availableRUB"], 2058 view["stat"]["blockedRUB"], 2059 ) 2060 ) 2061 ]) 2062 2063 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2064 return [ 2065 "| | | | | | | |\n", 2066 "| {:<27} | | | | | {:>19} | |\n".format( 2067 noTradeStr if noTradeStr else typeStr, 2068 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2069 ), 2070 ] 2071 2072 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2073 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2074 "{} [{}]".format(data["ticker"], data["figi"]), 2075 "{:.2f} ({:.2f}) {}".format( 2076 data["volume"], 2077 data["blocked"], 2078 data["currency"], 2079 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2080 data["volume"], 2081 data["blocked"], 2082 ), 2083 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2084 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2085 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2086 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2087 "{}{:.2f} {} ({}{:.2f}%)".format( 2088 "+" if data["profit"] > 0 else "", 2089 data["profit"], data["baseCurrencyName"], 2090 "+" if data["percentProfit"] > 0 else "", 2091 data["percentProfit"], 2092 ), 2093 ) 2094 2095 # --- Show currencies section: 2096 if view["stat"]["Currencies"]: 2097 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2098 for item in view["stat"]["Currencies"]: 2099 info.append(_InfoStr(item, showCurrencyName=True)) 2100 2101 else: 2102 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2103 2104 # --- Show shares section: 2105 if view["stat"]["Shares"]: 2106 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2107 2108 for item in view["stat"]["Shares"]: 2109 info.append(_InfoStr(item)) 2110 2111 else: 2112 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2113 2114 # --- Show bonds section: 2115 if view["stat"]["Bonds"]: 2116 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2117 2118 for item in view["stat"]["Bonds"]: 2119 info.append(_InfoStr(item)) 2120 2121 else: 2122 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2123 2124 # --- Show etfs section: 2125 if view["stat"]["Etfs"]: 2126 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2127 2128 for item in view["stat"]["Etfs"]: 2129 info.append(_InfoStr(item)) 2130 2131 else: 2132 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2133 2134 # --- Show futures section: 2135 if view["stat"]["Futures"]: 2136 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2137 2138 for item in view["stat"]["Futures"]: 2139 info.append(_InfoStr(item)) 2140 2141 else: 2142 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2143 2144 if details in ["full", "orders"]: 2145 # --- Show pending orders section: 2146 if view["stat"]["orders"]: 2147 info.extend([ 2148 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2149 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2150 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2151 ]) 2152 2153 for item in view["stat"]["orders"]: 2154 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2155 "{} [{}]".format(item["ticker"], item["figi"]), 2156 item["orderID"], 2157 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2158 "{} {} ({}{:.2f}%)".format( 2159 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2160 item["baseCurrencyName"], 2161 "+" if item["percentChanges"] > 0 else "", 2162 float(item["percentChanges"]), 2163 ), 2164 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2165 item["action"], 2166 item["type"], 2167 item["date"], 2168 )) 2169 2170 else: 2171 info.append("\n## Total pending limit-orders: 0\n") 2172 2173 # --- Show stop orders section: 2174 if view["stat"]["stopOrders"]: 2175 info.extend([ 2176 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2177 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2178 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2179 ]) 2180 2181 for item in view["stat"]["stopOrders"]: 2182 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2183 "{} [{}]".format(item["ticker"], item["figi"]), 2184 item["orderID"], 2185 item["lotsRequested"], 2186 "{} {} ({}{:.2f}%)".format( 2187 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2188 item["baseCurrencyName"], 2189 "+" if item["percentChanges"] > 0 else "", 2190 float(item["percentChanges"]), 2191 ), 2192 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2193 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2194 item["action"], 2195 item["type"], 2196 item["expType"], 2197 item["createDate"], 2198 item["expDate"], 2199 )) 2200 2201 else: 2202 info.append("\n## Total stop-orders: 0\n") 2203 2204 if details in ["full", "analytics"]: 2205 # -- Show analytics section: 2206 if view["stat"]["portfolioCostRUB"] > 0: 2207 info.extend([ 2208 "\n# Analytics\n" 2209 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2210 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2211 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2212 view["stat"]["totalChangesRUB"], 2213 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2214 view["stat"]["totalChangesPercentRUB"], 2215 ), 2216 "\n## Portfolio distribution by assets\n" 2217 "\n| Type | Uniques | Percent | Current cost |\n", 2218 "|------------|---------|---------|--------------------|\n", 2219 ]) 2220 2221 for key in view["analytics"]["distrByAssets"].keys(): 2222 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2223 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2224 key, 2225 view["analytics"]["distrByAssets"][key]["uniques"], 2226 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2227 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2228 )) 2229 2230 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2231 info.extend([ 2232 "\n## Portfolio distribution by companies\n" 2233 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2234 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2235 ]) 2236 2237 for company in view["analytics"]["distrByCompanies"].keys(): 2238 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2239 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2240 info.append("| {} | {:<7} | {:<18} |\n".format( 2241 "{}{}{}".format( 2242 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2243 company, 2244 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2245 ), 2246 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2247 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2248 )) 2249 2250 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2251 info.extend([ 2252 "\n## Portfolio distribution by sectors\n" 2253 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2254 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2255 ]) 2256 2257 for sector in view["analytics"]["distrBySectors"].keys(): 2258 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2259 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2260 sector, 2261 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2262 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2263 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2264 )) 2265 2266 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2267 info.extend([ 2268 "\n## Portfolio distribution by currencies\n" 2269 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2270 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2271 ]) 2272 2273 for curr in view["analytics"]["distrByCurrencies"].keys(): 2274 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2275 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2276 info.append("| {} | {:<7} | {:<18} |\n".format( 2277 "[{}] {}{}".format( 2278 curr, 2279 view["analytics"]["distrByCurrencies"][curr]["name"], 2280 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2281 ), 2282 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2283 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2284 )) 2285 2286 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2287 info.extend([ 2288 "\n## Portfolio distribution by countries\n" 2289 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2290 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2291 ]) 2292 2293 for country in view["analytics"]["distrByCountries"].keys(): 2294 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2295 nameLen = len(country) 2296 info.append("| {} | {:<7} | {:<18} |\n".format( 2297 "{}{}".format( 2298 country, 2299 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2300 ), 2301 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2302 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2303 )) 2304 2305 infoText = "".join(info) 2306 2307 uLogger.info(infoText) 2308 2309 if details == "full" and self.overviewFile: 2310 filename = self.overviewFile 2311 2312 elif details == "digest" and self.overviewDigestFile: 2313 filename = self.overviewDigestFile 2314 2315 elif details == "positions" and self.overviewPositionsFile: 2316 filename = self.overviewPositionsFile 2317 2318 elif details == "orders" and self.overviewOrdersFile: 2319 filename = self.overviewOrdersFile 2320 2321 elif details == "analytics" and self.overviewAnalyticsFile: 2322 filename = self.overviewAnalyticsFile 2323 2324 else: 2325 filename = "" 2326 2327 if filename: 2328 with open(filename, "w", encoding="UTF-8") as fH: 2329 fH.write(infoText) 2330 2331 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2332 2333 return view 2334 2335 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2336 """ 2337 Returns history operations between two given dates for current `accountId`. 2338 If `reportFile` string is not empty then also save human-readable report. 2339 Shows some statistical data of closed positions. 2340 2341 :param start: see docstring in `GetDatesAsString()` method 2342 :param end: see docstring in `GetDatesAsString()` method 2343 :param show: if `True` then also prints all records to the console. 2344 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2345 :return: original list of dictionaries with history of deals records from API ("operations" key): 2346 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2347 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2348 """ 2349 if self.accountId is None or not self.accountId: 2350 uLogger.error("Variable `accountId` must be defined for using this method!") 2351 raise Exception("Account ID required") 2352 2353 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2354 2355 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2356 2357 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2358 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2359 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2360 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2361 customStat = {} # custom statistics in additional to responseJSON 2362 2363 # --- output report in human-readable format: 2364 if show or self.reportFile: 2365 splitLine1 = "| | | | | |\n" # Summary section 2366 splitLine2 = "| | | | | | | | |\n" # Operations section 2367 nextDay = "" 2368 2369 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2370 2371 if len(ops) > 0: 2372 customStat = { 2373 "opsCount": 0, # total operations count 2374 "buyCount": 0, # buy operations 2375 "sellCount": 0, # sell operations 2376 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2377 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2378 "payIn": {"rub": 0.}, # Deposit brokerage account 2379 "payOut": {"rub": 0.}, # Withdrawals 2380 "divs": {"rub": 0.}, # Dividends income 2381 "coupons": {"rub": 0.}, # Coupon's income 2382 "brokerCom": {"rub": 0.}, # Service commissions 2383 "serviceCom": {"rub": 0.}, # Service commissions 2384 "marginCom": {"rub": 0.}, # Margin commissions 2385 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2386 } 2387 2388 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2389 for item in ops: 2390 if item["state"] == "OPERATION_STATE_EXECUTED": 2391 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2392 2393 # count buy operations: 2394 if "_BUY" in item["operationType"]: 2395 customStat["buyCount"] += 1 2396 2397 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2398 customStat["buyTotal"][item["payment"]["currency"]] += payment 2399 2400 else: 2401 customStat["buyTotal"][item["payment"]["currency"]] = payment 2402 2403 # count sell operations: 2404 elif "_SELL" in item["operationType"]: 2405 customStat["sellCount"] += 1 2406 2407 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2408 customStat["sellTotal"][item["payment"]["currency"]] += payment 2409 2410 else: 2411 customStat["sellTotal"][item["payment"]["currency"]] = payment 2412 2413 # count incoming operations: 2414 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2415 if item["payment"]["currency"] in customStat["payIn"].keys(): 2416 customStat["payIn"][item["payment"]["currency"]] += payment 2417 2418 else: 2419 customStat["payIn"][item["payment"]["currency"]] = payment 2420 2421 # count withdrawals operations: 2422 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2423 if item["payment"]["currency"] in customStat["payOut"].keys(): 2424 customStat["payOut"][item["payment"]["currency"]] += payment 2425 2426 else: 2427 customStat["payOut"][item["payment"]["currency"]] = payment 2428 2429 # count dividends income: 2430 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2431 if item["payment"]["currency"] in customStat["divs"].keys(): 2432 customStat["divs"][item["payment"]["currency"]] += payment 2433 2434 else: 2435 customStat["divs"][item["payment"]["currency"]] = payment 2436 2437 # count coupon's income: 2438 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2439 if item["payment"]["currency"] in customStat["coupons"].keys(): 2440 customStat["coupons"][item["payment"]["currency"]] += payment 2441 2442 else: 2443 customStat["coupons"][item["payment"]["currency"]] = payment 2444 2445 # count broker commissions: 2446 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2447 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2448 customStat["brokerCom"][item["payment"]["currency"]] += payment 2449 2450 else: 2451 customStat["brokerCom"][item["payment"]["currency"]] = payment 2452 2453 # count service commissions: 2454 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2455 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2456 customStat["serviceCom"][item["payment"]["currency"]] += payment 2457 2458 else: 2459 customStat["serviceCom"][item["payment"]["currency"]] = payment 2460 2461 # count margin commissions: 2462 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2463 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2464 customStat["marginCom"][item["payment"]["currency"]] += payment 2465 2466 else: 2467 customStat["marginCom"][item["payment"]["currency"]] = payment 2468 2469 # count withholding taxes: 2470 elif "_TAX" in item["operationType"]: 2471 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2472 customStat["allTaxes"][item["payment"]["currency"]] += payment 2473 2474 else: 2475 customStat["allTaxes"][item["payment"]["currency"]] = payment 2476 2477 else: 2478 continue 2479 2480 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2481 2482 # --- view "Actions" lines: 2483 info.extend([ 2484 "| 1 | 2 | 3 | 4 | 5 |\n", 2485 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2486 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2487 "| | Buy: {:<22} | {:<28} | | |\n".format( 2488 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2489 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2490 ), 2491 "| | Sell: {:<21} | {:<28} | | |\n".format( 2492 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2493 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2494 ), 2495 ]) 2496 2497 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2498 for key in opsKeys: 2499 if key == "rub": 2500 continue 2501 2502 info.extend([ 2503 "| | | {:<28} | | |\n".format( 2504 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2505 ), 2506 "| | | {:<28} | | |\n".format( 2507 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2508 ), 2509 ]) 2510 2511 info.append(splitLine1) 2512 2513 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2514 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2515 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2516 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2517 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2518 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2519 ) 2520 2521 # --- view "Payments" lines: 2522 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2523 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2524 2525 for key in paymentsKeys: 2526 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2527 2528 info.append(splitLine1) 2529 2530 # --- view "Commissions and taxes" lines: 2531 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2532 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2533 2534 for key in comKeys: 2535 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2536 2537 info.append(splitLine1) 2538 2539 info.extend([ 2540 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2541 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2542 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2543 ]) 2544 2545 else: 2546 info.append("Broker returned no operations during this period\n") 2547 2548 # --- view "Operations" section: 2549 for item in ops: 2550 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2551 continue 2552 2553 else: 2554 self.figi = item["figi"] if item["figi"] else "" 2555 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2556 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2557 2558 # group of deals during one day: 2559 if nextDay and item["date"].split("T")[0] != nextDay: 2560 info.append(splitLine2) 2561 nextDay = "" 2562 2563 else: 2564 nextDay = item["date"].split("T")[0] # saving current day for splitting 2565 2566 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2567 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2568 self.figi if self.figi else "—", 2569 instrument["ticker"] if instrument else "—", 2570 instrument["type"] if instrument else "—", 2571 item["quantity"] if int(item["quantity"]) > 0 else "—", 2572 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2573 TKS_OPERATION_STATES[item["state"]], 2574 TKS_OPERATION_TYPES[item["operationType"]], 2575 )) 2576 2577 infoText = "".join(info) 2578 2579 if show: 2580 uLogger.info(infoText) 2581 2582 if self.reportFile: 2583 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2584 fH.write(infoText) 2585 2586 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2587 2588 return ops, customStat 2589 2590 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2591 """ 2592 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2593 2594 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2595 Warning! Broker server used ISO UTC time by default. 2596 2597 If `historyFile` is not `None` then method save history to file, otherwise return only pandas dataframe. 2598 Also, `historyFile` used to update history with `onlyMissing` parameter. 2599 2600 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2601 2602 :param start: see docstring in `GetDatesAsString()` method. 2603 :param end: see docstring in `GetDatesAsString()` method. 2604 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2605 `"hour"`, `"day"`. Default: `"hour"`. 2606 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2607 False by default. Warning! History appends only from last candle to current time 2608 with always update last candle! 2609 :param csvSep: separator if csv-file is used, `,` by default. 2610 :param show: if `True` then also prints pandas dataframe to the console. 2611 :return: pandas dataframe with prices history. Headers of columns are defined by default: 2612 `["date", "time", "open", "high", "low", "close", "volume"]`. 2613 """ 2614 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2615 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2616 history = None # empty pandas object for history 2617 2618 if interval not in TKS_CANDLE_INTERVALS.keys(): 2619 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2620 raise Exception("Incorrect value") 2621 2622 if not (self.ticker or self.figi): 2623 uLogger.error("Ticker or FIGI must be defined!") 2624 raise Exception("Ticker or FIGI required") 2625 2626 if self.ticker and not self.figi: 2627 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2628 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2629 2630 if self.figi and not self.ticker: 2631 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2632 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2633 2634 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2635 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2636 if interval.lower() != "day": 2637 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2638 2639 delta = dtEnd - dtStart # current UTC time minus last time in file 2640 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2641 2642 # calculate history length in candles: 2643 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2644 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2645 length += 1 # to avoid fraction time 2646 2647 # calculate data blocks count: 2648 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2649 2650 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2651 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2652 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2653 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2654 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2655 2656 tempOld = None # pandas object for old history, if --only-missing key present 2657 lastTime = None # datetime object of last old candle in file 2658 2659 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2660 uLogger.debug("--only-missing key present, add only last missing candles...") 2661 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2662 2663 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2664 2665 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2666 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2667 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2668 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2669 2670 # get last datetime object from last string in file or minus 1 delta if file is empty: 2671 if len(tempOld) > 0: 2672 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2673 2674 else: 2675 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2676 2677 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2678 2679 responseJSONs = [] # raw history blocks of data 2680 2681 blockEnd = dtEnd 2682 for item in range(blocks): 2683 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2684 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2685 2686 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2687 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2688 )) 2689 2690 if blockStart == blockEnd: 2691 uLogger.debug("Skipped this zero-length block...") 2692 2693 else: 2694 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2695 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2696 self.body = str({ 2697 "figi": self.figi, 2698 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2699 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2700 "interval": TKS_CANDLE_INTERVALS[interval][0] 2701 }) 2702 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2703 2704 if "code" in responseJSON.keys(): 2705 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2706 2707 else: 2708 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2709 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2710 2711 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2712 2713 blockEnd = blockStart 2714 2715 printCount = len(responseJSONs) # candles to show in console 2716 if responseJSONs: 2717 tempHistory = pd.DataFrame( 2718 data={ 2719 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2720 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2721 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2722 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2723 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2724 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2725 "volume": [int(item["volume"]) for item in responseJSONs], 2726 }, 2727 index=range(len(responseJSONs)), 2728 columns=["date", "time", "open", "high", "low", "close", "volume"], 2729 ) 2730 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2731 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2732 2733 # append only newest candles to old history if --only-missing key present: 2734 if onlyMissing and tempOld is not None and lastTime is not None: 2735 index = 0 # find start index in tempHistory data: 2736 2737 for i, item in tempHistory.iterrows(): 2738 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2739 2740 if curTime == lastTime: 2741 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2742 index = i 2743 printCount = index + 1 2744 break 2745 2746 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2747 2748 else: 2749 history = tempHistory # if no `--only-missing` key then load full data from server 2750 2751 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2752 2753 if history is not None and not history.empty: 2754 if show: 2755 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2756 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2757 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2758 )) 2759 2760 else: 2761 uLogger.warning("Received an empty candles history!") 2762 2763 if self.historyFile is not None: 2764 if history is not None and not history.empty: 2765 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2766 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2767 2768 else: 2769 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2770 2771 else: 2772 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only pandas dataframe returns.") 2773 2774 return history 2775 2776 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2777 """ 2778 Load candles history from csv-file and return pandas dataframe object. 2779 2780 See also: `History()` and `ShowHistoryChart()` methods. 2781 2782 :param filePath: path to csv-file to open. 2783 """ 2784 loadedHistory = None # init candles data object 2785 2786 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2787 2788 if os.path.exists(filePath): 2789 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as pandas dataframe 2790 2791 tfStr = self.priceModel.FormattedDelta( 2792 self.priceModel.timeframe, 2793 "{days} days {hours}h {minutes}m {seconds}s", 2794 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2795 self.priceModel.timeframe, 2796 "{hours}h {minutes}m {seconds}s", 2797 ) 2798 2799 if loadedHistory is not None and not loadedHistory.empty: 2800 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2801 len(loadedHistory), 2802 tfStr, 2803 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2804 ) 2805 2806 else: 2807 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2808 2809 else: 2810 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2811 2812 return loadedHistory 2813 2814 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2815 """ 2816 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2817 2818 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2819 Default: `index.html` (both for interact and non-interact candlesticks chart). 2820 2821 See also: `History()` and `LoadHistory()` methods. 2822 2823 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2824 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2825 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2826 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2827 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2828 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2829 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2830 """ 2831 if isinstance(candles, str): 2832 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2833 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2834 2835 elif isinstance(candles, pd.DataFrame): 2836 self.priceModel.prices = candles # set candles chain from variable 2837 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2838 2839 if "datetime" not in candles.columns: 2840 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2841 2842 else: 2843 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2844 raise Exception("Incorrect value") 2845 2846 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2847 2848 if interact: 2849 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2850 2851 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2852 2853 else: 2854 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2855 2856 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2857 2858 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2859 2860 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2861 """ 2862 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2863 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2864 2865 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2866 2867 :param operation: string "Buy" or "Sell". 2868 :param lots: volume, integer count of lots >= 1. 2869 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2870 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2871 :param expDate: string "Undefined" by default or local date in future, 2872 it is a string with format `%Y-%m-%d %H:%M:%S`. 2873 :return: JSON with response from broker server. 2874 """ 2875 if self.accountId is None or not self.accountId: 2876 uLogger.error("Variable `accountId` must be defined for using this method!") 2877 raise Exception("Account ID required") 2878 2879 if operation is None or not operation or operation not in ("Buy", "Sell"): 2880 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2881 raise Exception("Incorrect value") 2882 2883 if lots is None or lots < 1: 2884 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2885 lots = 1 2886 2887 if tp is None or tp < 0: 2888 tp = 0 2889 2890 if sl is None or sl < 0: 2891 sl = 0 2892 2893 if expDate is None or not expDate: 2894 expDate = "Undefined" 2895 2896 if not (self.ticker or self.figi): 2897 uLogger.error("Ticker or FIGI must be defined!") 2898 raise Exception("Ticker or FIGI required") 2899 2900 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2901 self.ticker = instrument["ticker"] 2902 self.figi = instrument["figi"] 2903 2904 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2905 2906 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2907 self.body = str({ 2908 "figi": self.figi, 2909 "quantity": str(lots), 2910 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2911 "accountId": str(self.accountId), 2912 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2913 }) 2914 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2915 2916 if "orderId" in response.keys(): 2917 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2918 operation, response["orderId"], 2919 self.ticker, self.figi, lots, 2920 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2921 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2922 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2923 )) 2924 2925 else: 2926 uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.") 2927 2928 if tp > 0: 2929 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2930 2931 if sl > 0: 2932 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2933 2934 return response 2935 2936 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2937 """ 2938 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2939 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2940 2941 See also: `Order()` and `Trade()` docstrings. 2942 2943 :param lots: volume, integer count of lots >= 1. 2944 :param tp: float > 0, take profit price of stop-order. 2945 :param sl: float > 0, stop loss price of stop-order. 2946 :param expDate: it's a local date in future. 2947 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2948 :return: JSON with response from broker server. 2949 """ 2950 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2951 2952 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2953 """ 2954 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2955 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2956 2957 See also: `Order()` and `Trade()` docstrings. 2958 2959 :param lots: volume, integer count of lots >= 1. 2960 :param tp: float > 0, take profit price of stop-order. 2961 :param sl: float > 0, stop loss price of stop-order. 2962 :param expDate: it's a local date in the future. 2963 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2964 :return: JSON with response from broker server. 2965 """ 2966 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 2967 2968 def CloseTrades(self, tickers: list, portfolio: dict = None) -> None: 2969 """ 2970 Close position of given instruments. 2971 2972 :param tickers: tickers list of instruments that must be closed. 2973 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2974 This avoids unnecessary downloading data from the server. 2975 """ 2976 if not tickers: 2977 uLogger.info("Tickers list is empty, nothing to close.") 2978 2979 else: 2980 if portfolio is None or not portfolio: 2981 portfolio = self.Overview(show=False) 2982 2983 allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2984 uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers)) 2985 2986 for ticker in tickers: 2987 if ticker not in allOpenedTickers: 2988 uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker)) 2989 continue 2990 2991 # search open trade info about instrument by ticker: 2992 instrument = {} 2993 for iType in TKS_INSTRUMENTS: 2994 if instrument: 2995 break 2996 2997 for item in portfolio["stat"][iType]: 2998 if item["ticker"] == ticker: 2999 instrument = item 3000 break 3001 3002 if instrument: 3003 self.ticker = ticker 3004 self.figi = instrument["figi"] 3005 3006 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3007 self.ticker, 3008 self.figi, 3009 int(instrument["volume"]), 3010 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3011 )) 3012 3013 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3014 3015 if tradeLots > 0: 3016 if instrument["blocked"] > 0: 3017 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3018 instrument["blocked"], 3019 self.ticker, 3020 tradeLots, 3021 )) 3022 3023 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3024 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3025 3026 else: 3027 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3028 3029 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3030 """ 3031 Close all positions of given instruments with defined type. 3032 3033 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3034 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3035 This avoids unnecessary downloading data from the server. 3036 """ 3037 if iType not in TKS_INSTRUMENTS: 3038 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3039 3040 else: 3041 if portfolio is None or not portfolio: 3042 portfolio = self.Overview(show=False) 3043 3044 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3045 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3046 3047 if tickers and portfolio: 3048 self.CloseTrades(tickers, portfolio) 3049 3050 else: 3051 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3052 3053 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3054 """ 3055 Universal method to create market or limit orders with all available parameters for current `accountId`. 3056 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3057 3058 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3059 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3060 3061 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3062 then broker immediately open market order as you can do simple --buy or --sell operations! 3063 3064 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3065 When current price will go up or down to target price value then broker opens a limit order. 3066 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3067 3068 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3069 3070 :param operation: string "Buy" or "Sell". 3071 :param orderType: string "Limit" or "Stop". 3072 :param lots: volume, integer count of lots >= 1. 3073 :param targetPrice: target price > 0. This is open trade price for limit order. 3074 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3075 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3076 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3077 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3078 Stop loss order always executed by market price. 3079 :param expDate: string "Undefined" by default or local date in future. 3080 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3081 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3082 A limit order has no expiration date, it lasts until the end of the trading day. 3083 :return: JSON with response from broker server. 3084 """ 3085 if self.accountId is None or not self.accountId: 3086 uLogger.error("Variable `accountId` must be defined for using this method!") 3087 raise Exception("Account ID required") 3088 3089 if operation is None or not operation or operation not in ("Buy", "Sell"): 3090 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3091 raise Exception("Incorrect value") 3092 3093 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3094 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3095 raise Exception("Incorrect value") 3096 3097 if lots is None or lots < 1: 3098 uLogger.error("You must define trade volume > 0: integer count of lots!") 3099 raise Exception("Incorrect value") 3100 3101 if targetPrice is None or targetPrice <= 0: 3102 uLogger.error("Target price for limit-order must be greater than 0!") 3103 raise Exception("Incorrect value") 3104 3105 if limitPrice is None or limitPrice <= 0: 3106 limitPrice = targetPrice 3107 3108 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3109 stopType = "Limit" 3110 3111 if expDate is None or not expDate: 3112 expDate = "Undefined" 3113 3114 if not (self.ticker or self.figi): 3115 uLogger.error("Tocker or FIGI must be defined!") 3116 raise Exception("Ticker or FIGI required") 3117 3118 response = {} 3119 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3120 self.ticker = instrument["ticker"] 3121 self.figi = instrument["figi"] 3122 3123 if orderType == "Limit": 3124 uLogger.debug( 3125 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3126 self.ticker, self.figi, 3127 operation, lots, targetPrice, instrument["currency"], 3128 )) 3129 3130 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3131 self.body = str({ 3132 "figi": self.figi, 3133 "quantity": str(lots), 3134 "price": FloatToNano(targetPrice), 3135 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3136 "accountId": str(self.accountId), 3137 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3138 }) 3139 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3140 3141 if "orderId" in response.keys(): 3142 uLogger.info( 3143 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3144 response["orderId"], 3145 self.ticker, self.figi, 3146 operation, lots, targetPrice, instrument["currency"], 3147 )) 3148 3149 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3150 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3151 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3152 targetPrice, instrument["currency"], 3153 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3154 )) 3155 3156 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3157 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3158 targetPrice, instrument["currency"], 3159 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3160 )) 3161 3162 else: 3163 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3164 3165 if orderType == "Stop": 3166 uLogger.debug( 3167 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3168 self.ticker, self.figi, 3169 operation, lots, 3170 targetPrice, instrument["currency"], 3171 limitPrice, instrument["currency"], 3172 stopType, expDate, 3173 )) 3174 3175 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3176 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3177 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3178 3179 body = { 3180 "figi": self.figi, 3181 "quantity": str(lots), 3182 "price": FloatToNano(limitPrice), 3183 "stopPrice": FloatToNano(targetPrice), 3184 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3185 "accountId": str(self.accountId), 3186 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3187 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3188 } 3189 3190 if expDateUTC: 3191 body["expireDate"] = expDateUTC 3192 3193 self.body = str(body) 3194 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3195 3196 if "stopOrderId" in response.keys(): 3197 uLogger.info( 3198 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3199 response["stopOrderId"], 3200 self.ticker, self.figi, 3201 operation, lots, 3202 targetPrice, instrument["currency"], 3203 limitPrice, instrument["currency"], 3204 TKS_STOP_ORDER_TYPES[stopOrderType], 3205 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3206 )) 3207 3208 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3209 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3210 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3211 targetPrice, instrument["currency"], 3212 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3213 )) 3214 3215 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3216 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3217 targetPrice, instrument["currency"], 3218 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3219 )) 3220 3221 else: 3222 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3223 3224 return response 3225 3226 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3227 """ 3228 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3229 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3230 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3231 See also: `Order()` docstring. 3232 3233 :param lots: volume, integer count of lots >= 1. 3234 :param targetPrice: target price > 0. This is open trade price for limit order. 3235 :return: JSON with response from broker server. 3236 """ 3237 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3238 3239 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3240 """ 3241 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3242 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3243 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3244 target price value then broker opens a limit order. See also: `Order()` docstring. 3245 3246 :param lots: volume, integer count of lots >= 1. 3247 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3248 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3249 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3250 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3251 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3252 :param expDate: string "Undefined" by default or local date in future. 3253 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3254 This date is converting to UTC format for server. 3255 :return: JSON with response from broker server. 3256 """ 3257 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3258 3259 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3260 """ 3261 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3262 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3263 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3264 See also: `Order()` docstring. 3265 3266 :param lots: volume, integer count of lots >= 1. 3267 :param targetPrice: target price > 0. This is open trade price for limit order. 3268 :return: JSON with response from broker server. 3269 """ 3270 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3271 3272 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3273 """ 3274 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3275 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3276 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3277 target price value then broker opens a limit order. See also: `Order()` docstring. 3278 3279 :param lots: volume, integer count of lots >= 1. 3280 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3281 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3282 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3283 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3284 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3285 :param expDate: string "Undefined" by default or local date in future. 3286 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3287 This date is converting to UTC format for server. 3288 :return: JSON with response from broker server. 3289 """ 3290 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3291 3292 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3293 """ 3294 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3295 3296 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3297 :param allOrdersIDs: pre-received lists of all active pending orders. 3298 This avoids unnecessary downloading data from the server. 3299 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3300 """ 3301 if self.accountId is None or not self.accountId: 3302 uLogger.error("Variable `accountId` must be defined for using this method!") 3303 raise Exception("Account ID required") 3304 3305 if orderIDs: 3306 if allOrdersIDs is None or not allOrdersIDs: 3307 rawOrders = self.RequestPendingOrders() 3308 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3309 3310 if allStopOrdersIDs is None or not allStopOrdersIDs: 3311 rawStopOrders = self.RequestStopOrders() 3312 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3313 3314 for orderID in orderIDs: 3315 idInPendingOrders = orderID in allOrdersIDs 3316 idInStopOrders = orderID in allStopOrdersIDs 3317 3318 if not (idInPendingOrders or idInStopOrders): 3319 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3320 continue 3321 3322 else: 3323 if idInPendingOrders: 3324 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3325 3326 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3327 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3328 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3329 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3330 3331 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3332 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3333 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3334 3335 else: 3336 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3337 3338 elif idInStopOrders: 3339 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3340 3341 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3342 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3343 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3344 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3345 3346 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3347 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3348 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3349 3350 else: 3351 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3352 3353 else: 3354 continue 3355 3356 def CloseAllOrders(self) -> None: 3357 """ 3358 Gets a list of open pending and stop orders and cancel it all. 3359 """ 3360 rawOrders = self.RequestPendingOrders() 3361 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3362 lenOrders = len(allOrdersIDs) 3363 3364 rawStopOrders = self.RequestStopOrders() 3365 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3366 lenSOrders = len(allStopOrdersIDs) 3367 3368 if lenOrders > 0 or lenSOrders > 0: 3369 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3370 3371 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3372 3373 else: 3374 uLogger.info("Orders not found, nothing to cancel.") 3375 3376 def CloseAll(self, *args) -> None: 3377 """ 3378 Close all available (not blocked) opened trades and orders. 3379 3380 Also, you can select one or more keywords case-insensitive: 3381 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3382 3383 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3384 """ 3385 overview = self.Overview(show=False) # get all open trades info 3386 3387 if len(args) == 0: 3388 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3389 self.CloseAllOrders() # close all pending and stop orders 3390 3391 for iType in TKS_INSTRUMENTS: 3392 if iType != "Currencies": 3393 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3394 3395 else: 3396 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3397 lowerArgs = [x.lower() for x in args] 3398 3399 if "orders" in lowerArgs: 3400 self.CloseAllOrders() # close all pending and stop orders 3401 3402 for iType in TKS_INSTRUMENTS: 3403 if iType.lower() in lowerArgs and iType != "Currencies": 3404 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3405 3406 @staticmethod 3407 def ParseOrderParameters(operation, **inputParameters): 3408 """ 3409 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3410 3411 :param operation: string "Buy" or "Sell". 3412 :param inputParameters: this is dict of strings that looks like this 3413 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3414 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3415 "prices" key: one or more prices to open limit-orders 3416 Counts of values in lots and prices lists must be equals! 3417 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3418 """ 3419 # TODO: update order grid work with api v2 3420 pass 3421 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3422 # 3423 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3424 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3425 # raise Exception("Incorrect value") 3426 # 3427 # if "l" in inputParameters.keys(): 3428 # inputParameters["lots"] = inputParameters.pop("l") 3429 # 3430 # if "p" in inputParameters.keys(): 3431 # inputParameters["prices"] = inputParameters.pop("p") 3432 # 3433 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3434 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3435 # raise Exception("Incorrect value") 3436 # 3437 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3438 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3439 # 3440 # if len(lots) != len(prices): 3441 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3442 # raise Exception("Incorrect value") 3443 # 3444 # uLogger.debug("Extracted parameters for orders:") 3445 # uLogger.debug("lots = {}".format(lots)) 3446 # uLogger.debug("prices = {}".format(prices)) 3447 # 3448 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3449 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3450 # uLogger.debug("Order parameters: {}".format(result)) 3451 # 3452 # return result 3453 3454 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3455 """ 3456 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3457 3458 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3459 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3460 """ 3461 result = False 3462 msg = "Instrument not defined!" 3463 3464 if portfolio is None or not portfolio: 3465 portfolio = self.Overview(show=False) 3466 3467 if self.ticker: 3468 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3469 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3470 3471 for iType in TKS_INSTRUMENTS: 3472 for instrument in portfolio["stat"][iType]: 3473 if instrument["ticker"] == self.ticker: 3474 result = True 3475 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3476 break 3477 3478 elif self.figi: 3479 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3480 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3481 3482 for iType in TKS_INSTRUMENTS: 3483 for instrument in portfolio["stat"][iType]: 3484 if instrument["figi"] == self.figi: 3485 result = True 3486 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3487 break 3488 3489 else: 3490 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3491 3492 uLogger.debug(msg) 3493 3494 return result 3495 3496 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3497 """ 3498 Returns instrument is in the user's portfolio if it presents there. 3499 Instrument must be defined by `ticker` (highly priority) or `figi`. 3500 3501 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3502 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3503 """ 3504 result = None 3505 msg = "Instrument not defined!" 3506 3507 if portfolio is None or not portfolio: 3508 portfolio = self.Overview(show=False) 3509 3510 if self.ticker: 3511 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3512 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3513 3514 for iType in TKS_INSTRUMENTS: 3515 for instrument in portfolio["stat"][iType]: 3516 if instrument["ticker"] == self.ticker: 3517 result = instrument 3518 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3519 break 3520 3521 elif self.figi: 3522 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3523 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3524 3525 for iType in TKS_INSTRUMENTS: 3526 for instrument in portfolio["stat"][iType]: 3527 if instrument["figi"] == self.figi: 3528 result = instrument 3529 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3530 break 3531 3532 else: 3533 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3534 3535 uLogger.debug(msg) 3536 3537 return result 3538 3539 def RequestLimits(self) -> dict: 3540 """ 3541 Method for obtaining the available funds for withdrawal for current `accountId`. 3542 3543 See also: 3544 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3545 - `OverviewLimits()` method 3546 3547 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3548 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3549 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3550 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3551 """ 3552 if self.accountId is None or not self.accountId: 3553 uLogger.error("Variable `accountId` must be defined for using this method!") 3554 raise Exception("Account ID required") 3555 3556 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3557 3558 self.body = str({"accountId": self.accountId}) 3559 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3560 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3561 3562 uLogger.debug("Records about available funds for withdrawal successfully received") 3563 3564 return rawLimits 3565 3566 def OverviewLimits(self, show: bool = False) -> dict: 3567 """ 3568 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3569 3570 See also: `RequestLimits()`. 3571 3572 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3573 :return: dict with raw parsed data from server and some calculated statistics about it. 3574 """ 3575 if self.accountId is None or not self.accountId: 3576 uLogger.error("Variable `accountId` must be defined for using this method!") 3577 raise Exception("Account ID required") 3578 3579 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3580 3581 view = { 3582 "rawLimits": rawLimits, 3583 "limits": { # parsed data for every currency: 3584 "money": { # this is an array of portfolio currency positions 3585 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3586 }, 3587 "blocked": { # this is an array of blocked currency 3588 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3589 }, 3590 "blockedGuarantee": { # this is locked money under collateral for futures 3591 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3592 }, 3593 }, 3594 } 3595 3596 # --- Prepare text table with limits in human-readable format: 3597 if show: 3598 info = [ 3599 "# Withdrawal limits\n\n", 3600 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3601 "* **Account ID:** [{}]\n".format(self.accountId), 3602 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3603 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3604 ] 3605 3606 for curr in view["limits"]["money"].keys(): 3607 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3608 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3609 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3610 3611 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3612 "[{}]".format(curr), 3613 "{:.2f}".format(view["limits"]["money"][curr]), 3614 "{:.2f}".format(availableMoney), 3615 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3616 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3617 ) 3618 3619 if curr == "rub": 3620 info.insert(5, infoStr) # insert at first position in table and after headers 3621 3622 else: 3623 info.append(infoStr) 3624 3625 infoText = "".join(info) 3626 3627 uLogger.info(infoText) 3628 3629 if self.withdrawalLimitsFile: 3630 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3631 fH.write(infoText) 3632 3633 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3634 3635 return view 3636 3637 def RequestAccounts(self) -> dict: 3638 """ 3639 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3640 3641 See also: 3642 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3643 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3644 - `OverviewUserInfo()` method 3645 3646 :return: dict with raw data from server that contains accounts info. Example of dict: 3647 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3648 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3649 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3650 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3651 """ 3652 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3653 3654 self.body = str({}) 3655 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3656 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3657 3658 uLogger.debug("Records about available accounts successfully received") 3659 3660 return rawAccounts 3661 3662 def RequestUserInfo(self) -> dict: 3663 """ 3664 Method for requesting common user's information. 3665 3666 See also: 3667 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3668 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3669 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3670 - `OverviewUserInfo()` method 3671 3672 :return: dict with raw data from server that contains user's information. Example of dict: 3673 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3674 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3675 """ 3676 uLogger.debug("Requesting common user's information. Wait, please...") 3677 3678 self.body = str({}) 3679 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3680 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3681 3682 uLogger.debug("Records about current user successfully received") 3683 3684 return rawUserInfo 3685 3686 def RequestMarginStatus(self, accountId: str = None) -> dict: 3687 """ 3688 Method for requesting margin calculation for defined account ID. 3689 3690 See also: 3691 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3692 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3693 - `OverviewUserInfo()` method 3694 3695 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3696 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3697 Example of responses: 3698 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3699 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3700 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3701 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3702 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3703 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3704 """ 3705 if accountId is None or not accountId: 3706 if self.accountId is None or not self.accountId: 3707 uLogger.error("Variable `accountId` must be defined for using this method!") 3708 raise Exception("Account ID required") 3709 3710 else: 3711 accountId = self.accountId # use `self.accountId` (main ID) by default 3712 3713 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3714 3715 self.body = str({"accountId": accountId}) 3716 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3717 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3718 3719 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3720 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3721 rawMargin = {} 3722 3723 else: 3724 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3725 3726 return rawMargin 3727 3728 def RequestTariffLimits(self) -> dict: 3729 """ 3730 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3731 3732 See also: 3733 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3734 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3735 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3736 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3737 - `OverviewUserInfo()` method 3738 3739 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3740 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3741 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3742 """ 3743 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3744 3745 self.body = str({}) 3746 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3747 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3748 3749 uLogger.debug("Records with limits of current tariff successfully received") 3750 3751 return rawTariffLimits 3752 3753 def RequestBondCoupons(self, iJSON: dict) -> dict: 3754 """ 3755 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3756 then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". 3757 All dates are in UTC timezone. 3758 3759 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3760 Documentation: 3761 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3762 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3763 3764 See also: `ExtendBondsData()`. 3765 3766 :param iJSON: raw json data of a bond from broker server, example: `iJSON = self.iList["Bonds"][self.ticker]` 3767 If raw iJSON is not data of bond then server returns an error [400] with message: 3768 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3769 :return: dictionary with bond payment calendar. Response example: 3770 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3771 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3772 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3773 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3774 """ 3775 if iJSON["figi"] is None or not iJSON["figi"]: 3776 uLogger.error("FIGI must be defined for using this method!") 3777 raise Exception("FIGI required") 3778 3779 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3780 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3781 3782 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3783 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3784 self.figi, 3785 startDate, 3786 endDate, 3787 )) 3788 3789 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3790 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3791 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3792 3793 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3794 uLogger.warning("Instrument type is not bond!") 3795 3796 else: 3797 uLogger.debug("Records about bond payment calendar successfully received") 3798 3799 return calendar 3800 3801 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3802 """ 3803 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3804 pandas dataframe with more information about bonds: main info, current prices, bond payment calendar, 3805 coupon yields, current yields and some statistics etc. 3806 3807 WARNING! This is too long operation if a lot of bonds requested from broker server. 3808 3809 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3810 3811 :param instruments: list of strings with tickers or FIGIs. 3812 :param xlsx: if True then also exports pandas dataframe to xlsx-file `bondsXLSXFile`, default: `ext-bonds.xlsx`, 3813 for further used by data scientists or stock analytics. 3814 :return: wider pandas dataframe with more full and calculated data about bonds, than raw response from broker. 3815 In XLSX-file and pandas dataframe fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3816 """ 3817 if instruments is None or not instruments: 3818 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3819 raise Exception("Ticker or FIGI required") 3820 3821 if isinstance(instruments, str): 3822 instruments = [instruments] 3823 3824 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3825 3826 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3827 3828 iCount = len(uniqueInstruments) 3829 tooLong = iCount >= 20 3830 if tooLong: 3831 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3832 3833 bonds = None 3834 for i, self.figi in enumerate(uniqueInstruments): 3835 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3836 3837 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3838 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3839 rawBond = self.SearchByFIGI(requestPrice=True) 3840 3841 # Widen raw data with UTC current time (iData["actualDateTime"]): 3842 actualDate = datetime.now(tzutc()) 3843 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3844 3845 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3846 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3847 3848 # Replace some values with human-readable: 3849 iData["nominalCurrency"] = iData["nominal"]["currency"] 3850 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3851 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3852 iData["aciCurrency"] = iData["aciValue"]["currency"] 3853 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3854 iData["issueSize"] = int(iData["issueSize"]) 3855 iData["issueSizePlan"] = int(iData["issueSize"]) 3856 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3857 iData["minPriceIncrement"] = NanoToFloat(iData["minPriceIncrement"]["units"], iData["minPriceIncrement"]["nano"]) if "minPriceIncrement" in iData.keys() else 0. 3858 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3859 3860 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3861 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3862 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3863 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3864 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3865 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3866 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3867 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3868 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3869 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3870 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3871 3872 # Widen raw data with calendar data from `rawCalendar` values: 3873 calendarData = [] 3874 for item in iData["rawCalendar"]["events"]: 3875 calendarData.append({ 3876 "couponDate": item["couponDate"], 3877 "couponNumber": int(item["couponNumber"]), 3878 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3879 "payCurrency": item["payOneBond"]["currency"], 3880 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3881 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3882 "couponStartDate": item["couponStartDate"], 3883 "couponEndDate": item["couponEndDate"], 3884 "couponPeriod": item["couponPeriod"], 3885 }) 3886 3887 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3888 if "maturityDate" not in iData.keys(): 3889 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3890 3891 # Widen raw data with Coupon Rate. 3892 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3893 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3894 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3895 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3896 3897 # Widen raw data with Yield to Maturity (YTM) on current date. 3898 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3899 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3900 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3901 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3902 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3903 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3904 3905 iData["calendar"] = calendarData # adds calendar at the end 3906 3907 # Remove not used data: 3908 iData.pop("uid") 3909 iData.pop("positionUid") 3910 iData.pop("currentPrice") 3911 iData.pop("rawCalendar") 3912 3913 colNames = list(iData.keys()) 3914 if bonds is None: 3915 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3916 3917 else: 3918 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3919 3920 else: 3921 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3922 3923 processed = round(100 * (i + 1) / iCount, 1) 3924 if tooLong and processed % 5 == 0: 3925 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3926 3927 else: 3928 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3929 3930 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3931 3932 # Saving bonds from pandas dataframe to XLSX sheet: 3933 if xlsx and self.bondsXLSXFile: 3934 with pd.ExcelWriter( 3935 path=self.bondsXLSXFile, 3936 date_format=TKS_DATE_FORMAT, 3937 datetime_format=TKS_DATE_TIME_FORMAT, 3938 mode="w", 3939 ) as writer: 3940 bonds.to_excel( 3941 writer, 3942 sheet_name="Extended bonds data", 3943 index=True, 3944 encoding="UTF-8", 3945 freeze_panes=(1, 1), 3946 ) # saving as XLSX-file with freeze first row and column as headers 3947 3948 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3949 3950 return bonds 3951 3952 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 3953 """ 3954 Creates bond payments calendar as pandas dataframe, and you can also save it to the XLSX-file. 3955 3956 WARNING! This is too long operation if a lot of bonds requested from broker server. 3957 3958 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 3959 3960 :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains 3961 extended information about bonds: main info, current prices, bond payment calendar, 3962 coupon yields, current yields and some statistics etc. 3963 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 3964 :param xlsx: if True then also exports pandas dataframe to file `calendarFile` + `".xlsx"`, default: `calendar.xlsx`, 3965 for further used by data scientists or stock analytics. 3966 :return: pandas dataframe with only bond payments calendar data. 3967 """ 3968 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 3969 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 3970 3971 uLogger.debug("Generating bond payments calendar data. Wait, please...") 3972 3973 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 3974 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 3975 calendar = None 3976 for bond in extBonds.iterrows(): 3977 for item in bond[1]["calendar"]: 3978 cData = { 3979 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 3980 "couponDate": item["couponDate"], 3981 "figi": bond[1]["figi"], 3982 "ticker": bond[1]["ticker"], 3983 "name": bond[1]["name"], 3984 "couponNumber": item["couponNumber"], 3985 "payOneBond": item["payOneBond"], 3986 "payCurrency": item["payCurrency"], 3987 "couponType": item["couponType"], 3988 "couponPeriod": item["couponPeriod"], 3989 "fixDate": item["fixDate"], 3990 "couponStartDate": item["couponStartDate"], 3991 "couponEndDate": item["couponEndDate"], 3992 } 3993 3994 if calendar is None: 3995 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 3996 3997 else: 3998 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 3999 4000 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4001 4002 # Saving calendar from pandas dataframe to XLSX sheet: 4003 if xlsx: 4004 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4005 4006 with pd.ExcelWriter( 4007 path=xlsxCalendarFile, 4008 date_format=TKS_DATE_FORMAT, 4009 datetime_format=TKS_DATE_TIME_FORMAT, 4010 mode="w", 4011 ) as writer: 4012 humanReadable = calendar.copy(deep=True) 4013 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4014 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4015 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4016 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4017 humanReadable.columns = colNames # human-readable column names 4018 4019 humanReadable.to_excel( 4020 writer, 4021 sheet_name="Bond payments calendar", 4022 index=False, 4023 encoding="UTF-8", 4024 freeze_panes=(1, 2), 4025 ) # saving as XLSX-file with freeze first row and column as headers 4026 4027 del humanReadable # release df in memory 4028 4029 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4030 4031 return calendar 4032 4033 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4034 """ 4035 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4036 4037 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 4038 4039 :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains 4040 extended information about bonds: main info, current prices, bond payment calendar, 4041 coupon yields, current yields and some statistics etc. 4042 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4043 :param show: if `True` then also printing bonds payment calendar to the console, 4044 otherwise save to file `calendarFile` only. `False` by default. 4045 :return: multilines text in Markdown format with bonds payment calendar as a table. 4046 """ 4047 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4048 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4049 4050 infoText = "# Bond payments calendar\n\n" 4051 4052 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate pandas dataframe with full calendar data 4053 4054 if not calendar.empty: 4055 splitLine = "| | | | | | | | | |\n" 4056 4057 info = [ 4058 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4059 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4060 ] 4061 4062 newMonth = False 4063 notOneBond = calendar["figi"].nunique() > 1 4064 for i, bond in enumerate(calendar.iterrows()): 4065 if newMonth and notOneBond: 4066 info.append(splitLine) 4067 4068 info.append( 4069 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4070 " +" if bond[1]["paid"] else " —", 4071 bond[1]["couponDate"].split("T")[0], 4072 bond[1]["figi"], 4073 bond[1]["ticker"], 4074 bond[1]["couponNumber"], 4075 "{} {}".format( 4076 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4077 bond[1]["payCurrency"], 4078 ), 4079 bond[1]["couponType"], 4080 bond[1]["couponPeriod"], 4081 bond[1]["fixDate"].split("T")[0], 4082 ) 4083 ) 4084 4085 if i < len(calendar.values) - 1: 4086 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4087 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4088 newMonth = False if curDate.month == nextDate.month else True 4089 4090 else: 4091 newMonth = False 4092 4093 infoText += "".join(info) 4094 4095 if show: 4096 uLogger.info("{}".format(infoText)) 4097 4098 if self.calendarFile is not None: 4099 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4100 fH.write(infoText) 4101 4102 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4103 4104 else: 4105 infoText += "No data\n" 4106 4107 return infoText 4108 4109 def OverviewAccounts(self, show: bool = False) -> dict: 4110 """ 4111 Method for parsing and show simple table with all available user accounts. 4112 4113 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4114 4115 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4116 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4117 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4118 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4119 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4120 "closed": "—", "access": "Full access" }, ...}}` 4121 """ 4122 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4123 4124 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4125 accounts = { 4126 item["id"]: { 4127 "type": TKS_ACCOUNT_TYPES[item["type"]], 4128 "name": item["name"], 4129 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4130 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4131 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4132 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4133 } for item in rawAccounts["accounts"] 4134 } 4135 4136 # Raw and parsed data with some fields replaced in "stat" section: 4137 view = { 4138 "rawAccounts": rawAccounts, 4139 "stat": accounts, 4140 } 4141 4142 # --- Prepare simple text table with only accounts data in human-readable format: 4143 if show: 4144 info = [ 4145 "# User accounts\n\n", 4146 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4147 "| Account ID | Type | Status | Name |\n", 4148 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4149 ] 4150 4151 for account in view["stat"].keys(): 4152 info.extend([ 4153 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4154 account, 4155 view["stat"][account]["type"], 4156 view["stat"][account]["status"], 4157 view["stat"][account]["name"], 4158 ) 4159 ]) 4160 4161 infoText = "".join(info) 4162 4163 uLogger.info(infoText) 4164 4165 if self.userAccountsFile: 4166 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4167 fH.write(infoText) 4168 4169 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4170 4171 return view 4172 4173 def OverviewUserInfo(self, show: bool = False) -> dict: 4174 """ 4175 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4176 4177 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4178 4179 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4180 :return: dict with raw parsed data from server and some calculated statistics about it. 4181 """ 4182 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4183 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4184 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4185 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4186 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4187 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4188 4189 # This is dict with parsed common user data: 4190 userInfo = { 4191 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4192 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4193 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4194 "tariff": rawUserInfo["tariff"], 4195 } 4196 4197 # This is an array of dict with parsed margin statuses for every account IDs: 4198 margins = {} 4199 for accountId in accounts.keys(): 4200 if rawMargins[accountId]: 4201 margins[accountId] = { 4202 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4203 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4204 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4205 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4206 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4207 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4208 } 4209 4210 else: 4211 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4212 4213 unary = {} # unary-connection limits 4214 for item in rawTariffLimits["unaryLimits"]: 4215 if item["limitPerMinute"] in unary.keys(): 4216 unary[item["limitPerMinute"]].extend(item["methods"]) 4217 4218 else: 4219 unary[item["limitPerMinute"]] = item["methods"] 4220 4221 stream = {} # stream-connection limits 4222 for item in rawTariffLimits["streamLimits"]: 4223 if item["limit"] in stream.keys(): 4224 stream[item["limit"]].extend(item["streams"]) 4225 4226 else: 4227 stream[item["limit"]] = item["streams"] 4228 4229 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4230 limits = { 4231 "unary": unary, 4232 "stream": stream, 4233 } 4234 4235 # Raw and parsed data as an output result: 4236 view = { 4237 "rawUserInfo": rawUserInfo, 4238 "rawAccounts": rawAccounts, 4239 "rawMargins": rawMargins, 4240 "rawTariffLimits": rawTariffLimits, 4241 "stat": { 4242 "userInfo": userInfo, 4243 "accounts": accounts, 4244 "margins": margins, 4245 "limits": limits, 4246 }, 4247 } 4248 4249 # --- Prepare text table with user information in human-readable format: 4250 if show: 4251 info = [ 4252 "# Full user information\n\n", 4253 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4254 "## Common information\n\n", 4255 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4256 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4257 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4258 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4259 "\n## User accounts\n\n", 4260 ] 4261 4262 for account in view["stat"]["accounts"].keys(): 4263 info.extend([ 4264 "### ID: [{}]\n\n".format(account), 4265 "| Parameters | Values |\n", 4266 "|----------------------|--------------------------------------------------------------|\n", 4267 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4268 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4269 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4270 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4271 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4272 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4273 ]) 4274 4275 if margins[account]: 4276 info.extend([ 4277 "| Margin status: | Enabled |\n", 4278 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4279 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4280 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4281 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4282 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4283 ]) 4284 4285 else: 4286 info.append("| Margin status: | Disabled |\n\n") 4287 4288 info.extend([ 4289 "\n## Current user tariff limits\n", 4290 "\nSee also:\n", 4291 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4292 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4293 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4294 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4295 "\n### Unary limits\n", 4296 ]) 4297 4298 if unary: 4299 for key, values in sorted(unary.items()): 4300 info.append("\n* Max requests per minute: {}\n".format(key)) 4301 4302 for value in values: 4303 info.append(" - {}\n".format(value)) 4304 4305 else: 4306 info.append("\nNot available\n") 4307 4308 info.append("\n### Stream limits\n") 4309 4310 if stream: 4311 for key, values in sorted(stream.items()): 4312 info.append("\n* Max stream connections: {}\n".format(key)) 4313 4314 for value in values: 4315 info.append(" - {}\n".format(value)) 4316 4317 else: 4318 info.append("\nNot available\n") 4319 4320 infoText = "".join(info) 4321 4322 uLogger.info(infoText) 4323 4324 if self.userInfoFile: 4325 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4326 fH.write(infoText) 4327 4328 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4329 4330 return view 4331 4332 4333class Args: 4334 """ 4335 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4336 """ 4337 def __init__(self, **kwargs): 4338 self.__dict__.update(kwargs) 4339 4340 def __getattr__(self, item): 4341 return None 4342 4343 4344def ParseArgs(): 4345 """ 4346 Function get and parse command line keys. See examples: https://tim55667757.github.io/TKSBrokerAPI/ 4347 """ 4348 parser = ArgumentParser() # command-line string parser 4349 4350 parser.description = "TKSBrokerAPI is a python API to work with some methods of Tinkoff Open API using REST protocol. It can view history, orders and market information. Also, you can open orders and trades. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples" 4351 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4352 4353 # --- options: 4354 4355 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the program. `False` by default.") 4356 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4357 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4358 4359 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4360 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4361 4362 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4363 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4364 4365 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4366 4367 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4368 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4369 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4370 4371 parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4372 4373 # --- commands: 4374 4375 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4376 4377 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4378 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4379 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider pandas dataframe with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to xlsx-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4380 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4381 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4382 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present, and also saved to file `calendar.xlsx` by default. Also, if the `--output` key present then calendar saves to file, default: `calendar.md`. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4383 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4384 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4385 4386 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4387 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4388 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4389 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4390 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4391 4392 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4393 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4394 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4395 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4396 4397 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4398 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4399 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4400 4401 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4402 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4403 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4404 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4405 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4406 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4407 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4408 4409 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4410 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4411 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.") 4412 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.") 4413 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4414 4415 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4416 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4417 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4418 4419 cmdArgs = parser.parse_args() 4420 return cmdArgs 4421 4422 4423def Main(**kwargs): 4424 """ 4425 Main function for work with Tinkoff Open API service. It realizes simple logic: get a lot of options and execute one command. 4426 4427 See examples: https://tim55667757.github.io/TKSBrokerAPI/ 4428 """ 4429 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4430 4431 if args.debug_level: 4432 uLogger.level = 10 # always debug level by default 4433 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4434 4435 exitCode = 0 4436 start = datetime.now(tzutc()) 4437 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4438 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4439 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4440 )) 4441 4442 # trying to calculate full current version: 4443 buildVersion = __version__ 4444 try: 4445 v = version("tksbrokerapi") 4446 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4447 4448 except Exception: 4449 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4450 4451 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4452 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4453 4454 try: 4455 if args.version: 4456 print("TKSBrokerAPI {}".format(buildVersion)) 4457 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4458 4459 else: 4460 # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader` 4461 server = TinkoffBrokerServer( 4462 token=args.token, 4463 accountId=args.account_id, 4464 useCache=not args.no_cache, 4465 ) 4466 4467 # --- set some options: 4468 4469 if args.ticker: 4470 if args.ticker in server.aliasesKeys: 4471 server.ticker = server.aliases[args.ticker] # Replace some tickers with its aliases 4472 4473 else: 4474 server.ticker = args.ticker 4475 4476 if args.figi: 4477 server.figi = args.figi 4478 4479 if args.depth is not None: 4480 server.depth = args.depth 4481 4482 # --- do one of commands: 4483 4484 if args.list: 4485 if args.output is not None: 4486 server.instrumentsFile = args.output 4487 4488 server.ShowInstrumentsInfo(show=True) 4489 4490 elif args.list_xlsx: 4491 server.DumpInstrumentsAsXLSX(forceUpdate=False) 4492 4493 elif args.bonds_xlsx is not None: 4494 if args.output is not None: 4495 server.bondsXLSXFile = args.output 4496 4497 if len(args.bonds_xlsx) == 0: 4498 server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4499 4500 else: 4501 server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4502 4503 elif args.search: 4504 if args.output is not None: 4505 server.searchResultsFile = args.output 4506 4507 server.SearchInstruments(pattern=args.search[0], show=True) 4508 4509 elif args.info: 4510 if not (args.ticker or args.figi): 4511 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4512 raise Exception("Ticker or FIGI required") 4513 4514 if args.output is not None: 4515 server.infoFile = args.output 4516 4517 if args.ticker: 4518 server.SearchByTicker(requestPrice=True, show=True, debug=False) # show info and current prices by ticker name 4519 4520 else: 4521 server.SearchByFIGI(requestPrice=True, show=True, debug=False) # show info and current prices by FIGI id 4522 4523 elif args.calendar is not None: 4524 if args.output is not None: 4525 server.calendarFile = args.output 4526 4527 if len(args.calendar) == 0: 4528 bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4529 4530 else: 4531 bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4532 4533 server.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4534 4535 elif args.price: 4536 if not (args.ticker or args.figi): 4537 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4538 raise Exception("Ticker or FIGI required") 4539 4540 server.GetCurrentPrices(show=True) 4541 4542 elif args.prices is not None: 4543 if args.output is not None: 4544 server.pricesFile = args.output 4545 4546 server.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4547 4548 elif args.overview: 4549 if args.output is not None: 4550 server.overviewFile = args.output 4551 4552 server.Overview(show=True, details="full") 4553 4554 elif args.overview_digest: 4555 if args.output is not None: 4556 server.overviewDigestFile = args.output 4557 4558 server.Overview(show=True, details="digest") 4559 4560 elif args.overview_positions: 4561 if args.output is not None: 4562 server.overviewPositionsFile = args.output 4563 4564 server.Overview(show=True, details="positions") 4565 4566 elif args.overview_orders: 4567 if args.output is not None: 4568 server.overviewOrdersFile = args.output 4569 4570 server.Overview(show=True, details="orders") 4571 4572 elif args.overview_analytics: 4573 if args.output is not None: 4574 server.overviewAnalyticsFile = args.output 4575 4576 server.Overview(show=True, details="analytics") 4577 4578 elif args.deals is not None: 4579 if args.output is not None: 4580 server.reportFile = args.output 4581 4582 if 0 <= len(args.deals) < 3: 4583 server.Deals( 4584 start=args.deals[0] if len(args.deals) >= 1 else None, 4585 end=args.deals[1] if len(args.deals) == 2 else None, 4586 show=True, # Always show deals report in console 4587 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4588 ) 4589 4590 else: 4591 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4592 raise Exception("Incorrect value") 4593 4594 elif args.history is not None: 4595 if args.output is not None: 4596 server.historyFile = args.output 4597 4598 if 0 <= len(args.history) < 3: 4599 dataReceived = server.History( 4600 start=args.history[0] if len(args.history) >= 1 else None, 4601 end=args.history[1] if len(args.history) == 2 else None, 4602 interval="hour" if args.interval is None or not args.interval else args.interval, 4603 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4604 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4605 show=True, # shows all downloaded candles in console 4606 ) 4607 4608 if args.render_chart is not None and dataReceived is not None: 4609 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4610 4611 server.ShowHistoryChart( 4612 candles=dataReceived, 4613 interact=iChart, 4614 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4615 ) 4616 4617 else: 4618 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4619 raise Exception("Incorrect value") 4620 4621 elif args.load_history is not None: 4622 histData = server.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4623 4624 if args.render_chart is not None and histData is not None: 4625 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4626 server.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4627 4628 server.ShowHistoryChart( 4629 candles=histData, 4630 interact=iChart, 4631 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4632 ) 4633 4634 elif args.trade is not None: 4635 if 1 <= len(args.trade) <= 5: 4636 server.Trade( 4637 operation=args.trade[0], 4638 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4639 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4640 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4641 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4642 ) 4643 4644 else: 4645 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4646 4647 elif args.buy is not None: 4648 if 0 <= len(args.buy) <= 4: 4649 server.Buy( 4650 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4651 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4652 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4653 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4654 ) 4655 4656 else: 4657 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4658 4659 elif args.sell is not None: 4660 if 0 <= len(args.sell) <= 4: 4661 server.Sell( 4662 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4663 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4664 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4665 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4666 ) 4667 4668 else: 4669 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4670 4671 elif args.order: 4672 if 4 <= len(args.order) <= 7: 4673 server.Order( 4674 operation=args.order[0], 4675 orderType=args.order[1], 4676 lots=int(args.order[2]), 4677 targetPrice=float(args.order[3]), 4678 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4679 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4680 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4681 ) 4682 4683 else: 4684 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4685 4686 elif args.buy_limit: 4687 server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4688 4689 elif args.sell_limit: 4690 server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4691 4692 elif args.buy_stop: 4693 if 2 <= len(args.buy_stop) <= 7: 4694 server.BuyStop( 4695 lots=int(args.buy_stop[0]), 4696 targetPrice=float(args.buy_stop[1]), 4697 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4698 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4699 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4700 ) 4701 4702 else: 4703 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4704 4705 elif args.sell_stop: 4706 if 2 <= len(args.sell_stop) <= 7: 4707 server.SellStop( 4708 lots=int(args.sell_stop[0]), 4709 targetPrice=float(args.sell_stop[1]), 4710 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4711 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4712 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4713 ) 4714 4715 else: 4716 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4717 4718 # elif args.buy_order_grid is not None: 4719 # # update order grid work with api v2 4720 # if len(args.buy_order_grid) == 2: 4721 # orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4722 # 4723 # for order in orderParams: 4724 # server.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4725 # 4726 # else: 4727 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4728 # 4729 # elif args.sell_order_grid is not None: 4730 # # update order grid work with api v2 4731 # if len(args.sell_order_grid) >= 2: 4732 # orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4733 # 4734 # for order in orderParams: 4735 # server.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4736 # 4737 # else: 4738 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4739 4740 elif args.close_order is not None: 4741 server.CloseOrders(args.close_order) # close only one order 4742 4743 elif args.close_orders is not None: 4744 server.CloseOrders(args.close_orders) # close list of orders 4745 4746 elif args.close_trade: 4747 if not args.ticker: 4748 uLogger.error("`--ticker` key is required for this operation!") 4749 raise Exception("Ticker required") 4750 4751 server.CloseTrades([args.ticker]) # close only one trade 4752 4753 elif args.close_trades is not None: 4754 server.CloseTrades(args.close_trades) # close trades for list of tickers 4755 4756 elif args.close_all is not None: 4757 server.CloseAll(*args.close_all) 4758 4759 elif args.limits: 4760 if args.output is not None: 4761 server.withdrawalLimitsFile = args.output 4762 4763 server.OverviewLimits(show=True) 4764 4765 elif args.user_info: 4766 if args.output is not None: 4767 server.userInfoFile = args.output 4768 4769 server.OverviewUserInfo(show=True) 4770 4771 elif args.account: 4772 if args.output is not None: 4773 server.userAccountsFile = args.output 4774 4775 server.OverviewAccounts(show=True) 4776 4777 else: 4778 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4779 raise Exception("There is no command to execute") 4780 4781 except Exception: 4782 trace = tb.format_exc() 4783 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4784 if e in trace: 4785 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4786 break 4787 4788 uLogger.debug(trace) 4789 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4790 exitCode = 255 # an error occurred, must be open a ticket for this issue 4791 4792 finally: 4793 finish = datetime.now(tzutc()) 4794 4795 if exitCode == 0: 4796 uLogger.debug("All operations were finished success (summary code is 0).") 4797 4798 else: 4799 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4800 os.path.abspath(uLog.defaultLogFile), exitCode, 4801 )) 4802 4803 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4804 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4805 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4806 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4807 )) 4808 4809 if not kwargs: 4810 sys.exit(exitCode) 4811 4812 else: 4813 return exitCode 4814 4815 4816if __name__ == "__main__": 4817 Main()
78def NanoToFloat(units: str, nano: int) -> float: 79 """ 80 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 81 82 `NanoToFloat(units="2", nano=500000000) -> 2.5` 83 84 `NanoToFloat(units="0", nano=50000000) -> 0.05` 85 86 :param units: integer string or integer parameter that represents the integer part of number 87 :param nano: integer string or integer parameter that represents the fractional part of number 88 :return: float view of number 89 """ 90 return int(units) + int(nano) * NANO
Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:
NanoToFloat(units="2", nano=500000000) -> 2.5
NanoToFloat(units="0", nano=50000000) -> 0.05
Parameters
- units: integer string or integer parameter that represents the integer part of number
- nano: integer string or integer parameter that represents the fractional part of number
Returns
float view of number
93def FloatToNano(number: float) -> dict: 94 """ 95 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 96 97 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 98 99 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 100 101 :param number: float number 102 :return: nano-type view of number: `{"units": "string", "nano": integer}` 103 """ 104 splitByPoint = str(number).split(".") 105 frac = 0 106 107 if len(splitByPoint) > 1: 108 if len(splitByPoint[1]) <= 9: 109 frac = int("{}{}".format( 110 int(splitByPoint[1]), 111 "0" * (9 - len(splitByPoint[1])), 112 )) 113 114 if (number < 0) and (frac > 0): 115 frac = -frac 116 117 return {"units": str(int(number)), "nano": frac}
Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:
FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}
FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}
Parameters
- number: float number
Returns
nano-type view of number:
{"units": "string", "nano": integer}
120def GetDatesAsString(start: str = None, end: str = None) -> tuple: 121 """ 122 Create tuple of date and time strings with timezone parsed from user-friendly date. 123 124 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 125 126 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 127 An error exception will occur if input date has incorrect format. 128 129 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 130 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 131 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 132 Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago. 133 134 Also, you can use keywords for start if `end=None`: 135 `today` (from 00:00:00 to the end of current day), 136 `yesterday` (-1 day from 00:00:00 to 23:59:59), 137 `week` (-7 day from 00:00:00 to the end of current day), 138 `month` (-30 day from 00:00:00 to the end of current day), 139 `year` (-365 day from 00:00:00 to the end of current day), 140 141 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 142 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 143 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 144 """ 145 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 146 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 147 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 148 149 # time between start and the end of the current day: 150 if start is None or start.lower() == "today": 151 pass 152 153 # from start of the last day to the end of the last day: 154 elif start.lower() == "yesterday": 155 s -= timedelta(days=1) 156 e -= timedelta(days=1) 157 158 # week (-7 day from 00:00:00 to the end of the current day): 159 elif start.lower() == "week": 160 s -= timedelta(days=6) # +1 current day already taken into account 161 162 # month (-30 day from 00:00:00 to the end of current day): 163 elif start.lower() == "month": 164 s -= timedelta(days=29) # +1 current day already taken into account 165 166 # year (-365 day from 00:00:00 to the end of current day): 167 elif start.lower() == "year": 168 s -= timedelta(days=364) # +1 current day already taken into account 169 170 # -N days ago to the end of current day: 171 elif start.startswith('-') and start[1:].isdigit(): 172 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 173 174 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 175 else: 176 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 177 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 178 179 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 180 s = s.strftime(TKS_DATE_TIME_FORMAT) 181 e = e.strftime(TKS_DATE_TIME_FORMAT) 182 183 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 184 185 return s, e
Create tuple of date and time strings with timezone parsed from user-friendly date.
User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).
Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.
If start=None, end=None then return dates from yesterday to the end of the day.
If start=some_date_1, end=None then return dates from some_date_1 to the end of the day.
If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2.
Start day may be negative integer numbers: -1, -2, -3 - how many days ago.
Also, you can use keywords for start if end=None:
today (from 00:00:00 to the end of current day),
yesterday (-1 day from 00:00:00 to 23:59:59),
week (-7 day from 00:00:00 to the end of current day),
month (-30 day from 00:00:00 to the end of current day),
year (-365 day from 00:00:00 to the end of current day),
Returns
tuple with 2 strings
(start, end)dates in UTC ISO time format%Y-%m-%dT%H:%M:%SZfor OpenAPI. See date and time format here:TKSEnums.TKS_DATE_TIME_FORMAT. Example:("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.
188class TinkoffBrokerServer: 189 """ 190 This class implements methods to work with Tinkoff broker server. 191 192 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 193 194 About `token`: https://tinkoff.github.io/investAPI/token/ 195 """ 196 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 197 """ 198 Main class init. 199 200 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 201 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 202 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 203 :param useCache: use default cache file with raw data to use instead of `iList`. 204 True by default. Cache is auto-update if new day has come. 205 If you don't want to use cache and always updates raw data then set `useCache=False`. 206 :param defaultCache: path to default cache file. `dump.json` by default. 207 """ 208 if token is None or not token: 209 try: 210 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 211 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 212 213 except KeyError: 214 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 215 raise Exception("Token required") 216 217 else: 218 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 219 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 220 221 if accountId is None or not accountId: 222 try: 223 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 224 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 225 226 except KeyError: 227 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 228 229 else: 230 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 231 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 232 233 self.version = __version__ # duplicate here used TKSBrokerAPI main version 234 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 235 236 Latest version: https://pypi.org/project/tksbrokerapi/ 237 """ 238 239 self.aliases = TKS_TICKER_ALIASES 240 """Some aliases instead official tickers. 241 242 See also: `TKSEnums.TKS_TICKER_ALIASES` 243 """ 244 245 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 246 247 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 248 249 self.ticker = "" 250 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 251 252 See also: `SearchByTicker()`, `SearchInstruments()`. 253 """ 254 255 self.figi = "" 256 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 257 258 See also: `SearchByFIGI()`, `SearchInstruments()`. 259 """ 260 261 self.depth = 1 262 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 263 264 See also: `GetCurrentPrices()`. 265 """ 266 267 self.server = r"https://invest-public-api.tinkoff.ru/rest" 268 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 269 270 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 271 """ 272 273 uLogger.debug("Broker API server: {}".format(self.server)) 274 275 self.timeout = 15 276 """Server operations timeout in seconds. Default: `15`. 277 278 See also: `SendAPIRequest()`. 279 """ 280 281 self.headers = {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {}".format(self.token)} 282 """Headers which send in every request to broker server. Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 283 284 See also: `SendAPIRequest()`. 285 """ 286 287 self.body = None 288 """Request body which send to broker server. Default: `None`. 289 290 See also: `SendAPIRequest()`. 291 """ 292 293 self.historyFile = None 294 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only pandas dataframe. 295 296 See also: `History()`. 297 """ 298 299 self.htmlHistoryFile = "index.html" 300 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 301 302 See also: `ShowHistoryChart()`. 303 """ 304 305 self.instrumentsFile = "instruments.md" 306 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 307 308 See also: `ShowInstrumentsInfo()`. 309 """ 310 311 self.searchResultsFile = "search-results.md" 312 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 313 314 See also: `SearchInstruments()`. 315 """ 316 317 self.pricesFile = "prices.md" 318 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 319 320 See also: `GetListOfPrices()`. 321 """ 322 323 self.infoFile = "info.md" 324 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 325 326 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 327 """ 328 329 self.bondsXLSXFile = "ext-bonds.xlsx" 330 """Filename where wider pandas dataframe with more information about bonds: main info, current prices, 331 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 332 333 See also: `ExtendBondsData()`. 334 """ 335 336 self.calendarFile = "calendar.md" 337 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 338 339 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 340 341 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 342 """ 343 344 self.overviewFile = "overview.md" 345 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 346 347 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 348 """ 349 350 self.overviewDigestFile = "overview-digest.md" 351 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 352 353 See also: `Overview()` with parameter `details="digest"`. 354 """ 355 356 self.overviewPositionsFile = "overview-positions.md" 357 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 358 359 See also: `Overview()` with parameter `details="positions"`. 360 """ 361 362 self.overviewOrdersFile = "overview-orders.md" 363 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 364 365 See also: `Overview()` with parameter `details="orders"`. 366 """ 367 368 self.overviewAnalyticsFile = "overview-analytics.md" 369 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 370 371 See also: `Overview()` with parameter `details="analytics"`. 372 """ 373 374 self.reportFile = "deals.md" 375 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 376 377 See also: `Deals()`. 378 """ 379 380 self.withdrawalLimitsFile = "limits.md" 381 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 382 383 See also: `OverviewLimits()` and `RequestLimits()`. 384 """ 385 386 self.userInfoFile = "user-info.md" 387 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 388 389 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 390 """ 391 392 self.userAccountsFile = "accounts.md" 393 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 394 395 See also: `OverviewAccounts()`, `RequestAccounts()`. 396 """ 397 398 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 399 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 400 401 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 402 403 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 404 """ 405 406 self.iList = None # init iList for raw instruments data 407 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 408 409 See also: `Listing()`, `DumpInstruments()`. 410 """ 411 412 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 413 if useCache: 414 if os.path.exists(self.iListDumpFile): 415 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 416 curTime = datetime.now(tzutc()) 417 418 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 419 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 420 421 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 422 423 else: 424 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 425 426 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 427 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 428 429 else: 430 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 431 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 432 433 else: 434 self.iList = self.Listing() # request new raw instruments data from broker server 435 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 436 437 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 438 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 439 440 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 441 """ 442 443 @staticmethod 444 def _ParseJSON(rawData="{}", debug: bool = False) -> dict: 445 """ 446 Parse JSON from response string. 447 448 :param rawData: this is a string with JSON-formatted text. 449 :param debug: if `True` then print more debug information. 450 :return: JSON (dictionary), parsed from server response string. 451 """ 452 if debug: 453 uLogger.debug("Raw text body:") 454 uLogger.debug(rawData) 455 456 responseJSON = json.loads(rawData) if rawData else {} 457 458 if debug: 459 uLogger.debug("JSON formatted:") 460 for jsonLine in json.dumps(responseJSON, indent=4).split('\n'): 461 uLogger.debug(jsonLine) 462 463 return responseJSON 464 465 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 466 """ 467 Send GET or POST request to broker server and receive JSON object. 468 469 self.header: must be defining with dictionary of headers. 470 self.body: if define then used as request body. None by default. 471 self.timeout: global request timeout, 15 seconds by default. 472 :param url: url with REST request. 473 :param reqType: send "GET" or "POST" request. "GET" by default. 474 :param retry: how many times retry after first request if an 5xx server errors occurred. 475 :param pause: sleep time in seconds between retries. 476 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 477 :return: response JSON (dictionary) from broker. 478 """ 479 if reqType not in ("GET", "POST"): 480 uLogger.error("You can define request type: 'GET' or 'POST'!") 481 raise Exception("Incorrect value") 482 483 if debug: 484 uLogger.debug("Request parameters:") 485 uLogger.debug(" - REST API URL: {}".format(url)) 486 uLogger.debug(" - request type: {}".format(reqType)) 487 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 488 uLogger.debug(" - body: {}".format(self.body)) 489 490 # fast hack to avoid all operations with some tickers/FIGI 491 responseJSON = {} 492 oK = True 493 for item in self.exclude: 494 if item in url: 495 if debug: 496 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 497 498 oK = False 499 break 500 501 if oK: 502 counter = 0 503 response = None 504 errMsg = "" 505 506 while not response and counter <= retry: 507 if reqType == "GET": 508 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 509 510 if reqType == "POST": 511 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 512 513 if debug: 514 uLogger.debug("Response:") 515 uLogger.debug(" - status code: {}".format(response.status_code)) 516 uLogger.debug(" - reason: {}".format(response.reason)) 517 uLogger.debug(" - body length: {}".format(len(response.text))) 518 uLogger.debug(" - headers: {}".format(response.headers)) 519 520 # Server returns some headers: 521 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 522 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 523 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 524 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 525 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 526 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 527 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 528 sleep(rateLimitWait) 529 530 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 531 if 400 <= response.status_code < 500: 532 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 533 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 534 counter = retry + 1 535 536 if 500 <= response.status_code < 600: 537 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 538 uLogger.debug(" - not oK, {}".format(errMsg)) 539 counter += 1 540 541 if counter <= retry: 542 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 543 sleep(pause) 544 545 responseJSON = self._ParseJSON(response.text) 546 547 if errMsg: 548 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 549 uLogger.error(" - not oK, {}".format(errMsg)) 550 551 return responseJSON 552 553 def _IUpdater(self, iType: str) -> tuple: 554 """ 555 Request instrument by type from server. See available API methods for instruments: 556 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 557 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 558 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 559 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 560 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 561 562 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 563 :return: tuple with iType name and list of available instruments of current type for defined user token. 564 """ 565 result = [] 566 567 if iType in TKS_INSTRUMENTS: 568 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 569 570 # all instruments have the same body in API v2 requests: 571 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 572 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 573 result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"] 574 575 return iType, result 576 577 def _IWrapper(self, kwargs): 578 """ 579 Wrapper runs instrument's update method `_IUpdater()`. 580 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 581 """ 582 return self._IUpdater(**kwargs) 583 584 def Listing(self) -> dict: 585 """ 586 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 587 588 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 589 """ 590 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 591 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 592 593 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 594 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 595 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 596 597 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 598 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 599 poolUpdater.close() 600 601 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 602 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 603 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 604 605 # calculate minimum price increment (step) for all instruments and set up instrument's type: 606 for iType in iList.keys(): 607 for ticker in iList[iType]: 608 iList[iType][ticker]["type"] = iType 609 610 if "minPriceIncrement" in iList[iType][ticker].keys(): 611 iList[iType][ticker]["step"] = NanoToFloat( 612 iList[iType][ticker]["minPriceIncrement"]["units"], 613 iList[iType][ticker]["minPriceIncrement"]["nano"], 614 ) 615 616 else: 617 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 618 619 return iList 620 621 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 622 """ 623 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 624 625 See also: `DumpInstruments()`, `Listing()`. 626 627 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 628 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 629 """ 630 if self.iListDumpFile is None or not self.iListDumpFile: 631 uLogger.error("Output name of dump file must be defined!") 632 raise Exception("Filename required") 633 634 if not self.iList or forceUpdate: 635 self.iList = self.Listing() 636 637 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 638 639 # Save as XLSX with separated sheets for every type of instruments: 640 with pd.ExcelWriter( 641 path=xlsxDumpFile, 642 date_format=TKS_DATE_FORMAT, 643 datetime_format=TKS_DATE_TIME_FORMAT, 644 mode="w", 645 ) as writer: 646 for iType in TKS_INSTRUMENTS: 647 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 648 df = df[sorted(df)] # sorted by column names 649 df = df.applymap( 650 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 651 na_action="ignore", 652 ) # converting numbers from nano-type to float in every cell 653 df.to_excel( 654 writer, 655 sheet_name=iType, 656 encoding="UTF-8", 657 freeze_panes=(1, 1), 658 ) # saving as XLSX-file with freeze first row and column as headers 659 660 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 661 662 def DumpInstruments(self, forceUpdate: bool = True) -> str: 663 """ 664 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 665 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 666 667 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 668 669 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 670 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 671 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 672 """ 673 if self.iListDumpFile is None or not self.iListDumpFile: 674 uLogger.error("Output name of dump file must be defined!") 675 raise Exception("Filename required") 676 677 if not self.iList or forceUpdate: 678 self.iList = self.Listing() 679 680 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 681 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 682 fH.write(jsonDump) 683 684 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 685 686 return jsonDump 687 688 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 689 """ 690 Show information about one instrument defined by json data and prints it in Markdown format. 691 692 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 693 694 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 695 :param show: if `True` then also printing information about instrument and its current price. 696 :return: multilines text in Markdown format with information about one instrument. 697 """ 698 splitLine = "| | |\n" 699 infoText = "" 700 701 if iJSON is not None and iJSON and isinstance(iJSON, dict): 702 info = [ 703 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 704 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 705 "| Parameters | Values |\n", 706 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 707 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 708 "| Full name: | {:<54} |\n".format(iJSON["name"]), 709 ] 710 711 if "sector" in iJSON.keys() and iJSON["sector"]: 712 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 713 714 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 715 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 716 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 717 ))) 718 719 info.extend([ 720 splitLine, 721 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 722 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 723 ]) 724 725 if "isin" in iJSON.keys() and iJSON["isin"]: 726 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 727 728 if "classCode" in iJSON.keys(): 729 info.append("| Class Code: | {:<54} |\n".format(iJSON["classCode"])) 730 731 info.extend([ 732 splitLine, 733 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 734 splitLine, 735 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 736 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 737 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 738 ]) 739 740 if iJSON["figi"]: 741 self.figi = iJSON["figi"] 742 iJSON = iJSON | self.RequestTradingStatus() 743 744 info.extend([ 745 splitLine, 746 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 747 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 748 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 749 ]) 750 751 info.append(splitLine) 752 753 if "type" in iJSON.keys() and iJSON["type"]: 754 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 755 756 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 757 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 758 759 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 760 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 761 762 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 763 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 764 765 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 766 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 767 768 if "focusType" in iJSON.keys() and iJSON["focusType"]: 769 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 770 771 if "assetType" in iJSON.keys() and iJSON["assetType"]: 772 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 773 774 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 775 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 776 777 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 778 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 779 780 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 781 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 782 783 if "currency" in iJSON.keys(): 784 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 785 786 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 787 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 788 789 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 790 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 791 792 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 793 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 794 795 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 796 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 797 798 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 799 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 800 801 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 802 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 803 804 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 805 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 806 807 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 808 info.append("| Perpetual bond: | Yes |\n") 809 810 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 811 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 812 813 iExt = None 814 if iJSON["type"] == "Bonds": 815 info.extend([ 816 splitLine, 817 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 818 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 819 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 820 iJSON["nominal"]["currency"], 821 )), 822 ]) 823 824 if "floatingCouponFlag" in iJSON.keys(): 825 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 826 827 if "amortizationFlag" in iJSON.keys(): 828 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 829 830 info.append(splitLine) 831 832 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 833 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 834 835 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 836 837 info.extend([ 838 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 839 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 840 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 841 ]) 842 843 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 844 info.append("| Current Accrued Interest (ACI): | {:<54} |\n".format("{:.2f} {}".format( 845 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 846 iJSON["aciValue"]["currency"] 847 ))) 848 849 if "currentPrice" in iJSON.keys(): 850 info.append(splitLine) 851 852 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 853 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 854 855 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 856 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 857 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 858 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 859 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 860 861 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 862 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 863 864 info.extend([ 865 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 866 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 867 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 868 )), 869 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 870 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 871 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 872 )), 873 "| Changes between last deal price and last close | {:<54} |\n".format( 874 "{:.2f}%{}".format( 875 iJSON["currentPrice"]["changes"], 876 " ({}{:.2f} {})".format( 877 "+" if bondChangesDelta > 0 else "", 878 bondChangesDelta, 879 aciCurrency 880 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 881 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 882 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 883 currency 884 ), 885 ) 886 ), 887 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 888 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 889 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 890 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 891 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 892 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 893 )), 894 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 895 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 896 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 897 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 898 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 899 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 900 )), 901 ]) 902 903 if "lot" in iJSON.keys(): 904 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 905 906 if "step" in iJSON.keys() and iJSON["step"] != 0: 907 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 908 909 # Add bond payment calendar: 910 if iJSON["type"] == "Bonds": 911 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 912 info.extend(["\n", strCalendar]) 913 914 infoText += "".join(info) 915 916 if show: 917 uLogger.info("{}".format(infoText)) 918 919 else: 920 uLogger.debug("{}".format(infoText)) 921 922 if self.infoFile is not None: 923 with open(self.infoFile, "w", encoding="UTF-8") as fH: 924 fH.write(infoText) 925 926 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 927 928 return infoText 929 930 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 931 """ 932 Search and return raw broker's information about instrument by its ticker. 933 `ticker` must be defined! If debug=True then print all debug messages. 934 935 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 936 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 937 :param debug: if `True` then print all debug console messages. 938 :return: JSON formatted data with information about instrument. 939 """ 940 tickerJSON = {} 941 if debug: 942 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 943 944 if not self.ticker: 945 uLogger.warning("self.ticker variable is not be empty!") 946 947 else: 948 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 949 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 950 raise Exception("Instrument not allowed") 951 952 if not self.iList: 953 self.iList = self.Listing() 954 955 if self.ticker in self.iList["Shares"].keys(): 956 tickerJSON = self.iList["Shares"][self.ticker] 957 if debug: 958 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 959 960 elif self.ticker in self.iList["Currencies"].keys(): 961 tickerJSON = self.iList["Currencies"][self.ticker] 962 if debug: 963 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 964 965 elif self.ticker in self.iList["Bonds"].keys(): 966 tickerJSON = self.iList["Bonds"][self.ticker] 967 if debug: 968 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 969 970 elif self.ticker in self.iList["Etfs"].keys(): 971 tickerJSON = self.iList["Etfs"][self.ticker] 972 if debug: 973 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 974 975 elif self.ticker in self.iList["Futures"].keys(): 976 tickerJSON = self.iList["Futures"][self.ticker] 977 if debug: 978 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 979 980 if tickerJSON: 981 self.figi = tickerJSON["figi"] 982 983 if requestPrice: 984 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 985 986 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 987 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 988 989 else: 990 tickerJSON["currentPrice"]["changes"] = 0 991 992 if show: 993 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 994 995 else: 996 if show: 997 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 998 999 return tickerJSON 1000 1001 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1002 """ 1003 Search and return raw broker's information about instrument by its FIGI. 1004 `figi` must be defined! If debug=True then print all debug messages. 1005 1006 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1007 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1008 :param debug: if `True` then print all debug console messages. 1009 :return: JSON formatted data with information about instrument. 1010 """ 1011 figiJSON = {} 1012 if debug: 1013 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1014 1015 if not self.figi: 1016 uLogger.warning("self.figi variable is not be empty!") 1017 1018 else: 1019 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1020 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1021 raise Exception("Instrument not allowed") 1022 1023 if not self.iList: 1024 self.iList = self.Listing() 1025 1026 for item in self.iList["Shares"].keys(): 1027 if self.figi == self.iList["Shares"][item]["figi"]: 1028 figiJSON = self.iList["Shares"][item] 1029 1030 if debug: 1031 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1032 1033 break 1034 1035 if not figiJSON: 1036 for item in self.iList["Currencies"].keys(): 1037 if self.figi == self.iList["Currencies"][item]["figi"]: 1038 figiJSON = self.iList["Currencies"][item] 1039 1040 if debug: 1041 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1042 1043 break 1044 1045 if not figiJSON: 1046 for item in self.iList["Bonds"].keys(): 1047 if self.figi == self.iList["Bonds"][item]["figi"]: 1048 figiJSON = self.iList["Bonds"][item] 1049 1050 if debug: 1051 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1052 1053 break 1054 1055 if not figiJSON: 1056 for item in self.iList["Etfs"].keys(): 1057 if self.figi == self.iList["Etfs"][item]["figi"]: 1058 figiJSON = self.iList["Etfs"][item] 1059 1060 if debug: 1061 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1062 1063 break 1064 1065 if not figiJSON: 1066 for item in self.iList["Futures"].keys(): 1067 if self.figi == self.iList["Futures"][item]["figi"]: 1068 figiJSON = self.iList["Futures"][item] 1069 1070 if debug: 1071 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1072 1073 break 1074 1075 if figiJSON: 1076 self.figi = figiJSON["figi"] 1077 self.ticker = figiJSON["ticker"] 1078 1079 if requestPrice: 1080 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1081 1082 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1083 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1084 1085 else: 1086 figiJSON["currentPrice"]["changes"] = 0 1087 1088 if show: 1089 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1090 1091 else: 1092 if show: 1093 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1094 1095 return figiJSON 1096 1097 def GetCurrentPrices(self, show: bool = True) -> dict: 1098 """ 1099 Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record: 1100 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1101 1102 See also: 1103 1104 :param show: if `True` then print DOM to log and console. 1105 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1106 """ 1107 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1108 1109 if self.depth < 1: 1110 uLogger.error("Depth of Market (DOM) must be >=1!") 1111 raise Exception("Incorrect value") 1112 1113 if not (self.ticker or self.figi): 1114 uLogger.error("self.ticker or self.figi variables must be defined!") 1115 raise Exception("Ticker or FIGI required") 1116 1117 if self.ticker and not self.figi: 1118 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1119 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1120 1121 if not self.ticker and self.figi: 1122 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1123 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1124 1125 if not self.figi: 1126 uLogger.error("FIGI is not defined!") 1127 raise Exception("Ticker or FIGI required") 1128 1129 else: 1130 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1131 1132 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1133 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1134 self.body = str({"figi": self.figi, "depth": self.depth}) 1135 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") 1136 1137 if pricesResponse: 1138 # list of dicts with sellers orders: 1139 prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1140 1141 # list of dicts with buyers orders: 1142 prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1143 1144 # max price of instrument at this time: 1145 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1146 1147 # min price of instrument at this time: 1148 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1149 1150 # last price of deal with instrument: 1151 prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0 1152 1153 # last close price of instrument: 1154 prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0 1155 1156 else: 1157 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1158 uLogger.debug("Server response: {}".format(pricesResponse)) 1159 1160 if show: 1161 if prices["buy"] or prices["sell"]: 1162 info = [ 1163 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1164 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1165 self.ticker, 1166 self.figi, 1167 self.depth, 1168 ), 1169 uLog.sepShort, "\n", 1170 " Orders of Buyers | Orders of Sellers\n", 1171 uLog.sepShort, "\n", 1172 " Sell prices (vol.) | Buy prices (vol.)\n", 1173 uLog.sepShort, "\n", 1174 ] 1175 1176 if not prices["buy"]: 1177 info.append(" | No orders!\n") 1178 sumBuy = 0 1179 1180 else: 1181 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1182 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1183 for item in maxMinSorted: 1184 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1185 1186 if not prices["sell"]: 1187 info.append("No orders! |\n") 1188 sumSell = 0 1189 1190 else: 1191 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1192 for item in prices["sell"]: 1193 info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1194 1195 info.extend([ 1196 uLog.sepShort, "\n", 1197 "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1198 uLog.sepShort, "\n", 1199 ]) 1200 1201 infoText = "".join(info) 1202 1203 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1204 1205 else: 1206 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1207 1208 return prices 1209 1210 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1211 """ 1212 This method get and show information about all available broker instruments for current user account. 1213 If `instrumentsFile` string is not empty then also save information to this file. 1214 1215 :param show: if `True` then print results to console, if `False` - print only to file. 1216 :return: multi-lines string with all available broker instruments 1217 """ 1218 if not self.iList: 1219 self.iList = self.Listing() 1220 1221 info = [ 1222 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1223 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1224 ] 1225 1226 # add instruments count by type: 1227 for iType in self.iList.keys(): 1228 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1229 1230 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1231 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1232 1233 # generating info tables with all instruments by type: 1234 for iType in self.iList.keys(): 1235 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1236 1237 for instrument in self.iList[iType].keys(): 1238 iName = self.iList[iType][instrument]["name"] # instrument's name 1239 if len(iName) > 57: 1240 iName = "{}...".format(iName[:54]) # right trim for a long string 1241 1242 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1243 self.iList[iType][instrument]["ticker"], 1244 iName, 1245 self.iList[iType][instrument]["figi"], 1246 self.iList[iType][instrument]["currency"], 1247 self.iList[iType][instrument]["lot"], 1248 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1249 )) 1250 1251 infoText = "".join(info) 1252 1253 if show: 1254 uLogger.info(infoText) 1255 1256 if self.instrumentsFile: 1257 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1258 fH.write(infoText) 1259 1260 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1261 1262 return infoText 1263 1264 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1265 """ 1266 This method search and show information about instruments by part of its ticker, FIGI or name. 1267 If `searchResultsFile` string is not empty then also save information to this file. 1268 1269 :param pattern: string with part of ticker, FIGI or instrument's name. 1270 :param show: if `True` then print results to console, if `False` - return list of result only. 1271 :return: list of dictionaries with all found instruments. 1272 """ 1273 if not self.iList: 1274 self.iList = self.Listing() 1275 1276 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1277 compiledPattern = re.compile(pattern, re.IGNORECASE) 1278 1279 for iType in self.iList: 1280 for instrument in self.iList[iType].values(): 1281 searchResult = compiledPattern.search(" ".join( 1282 [instrument["ticker"], instrument["figi"], instrument["name"]] 1283 )) 1284 1285 if searchResult: 1286 searchResults[iType][instrument["ticker"]] = instrument 1287 1288 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1289 info = [ 1290 "# Search results\n\n", 1291 "* **Search pattern:** [{}]\n".format(pattern), 1292 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1293 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1294 ] 1295 infoShort = info[:] 1296 1297 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1298 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1299 skippedLine = "| ... | ... | ... | ... |\n" 1300 1301 if resultsLen == 0: 1302 info.append("\nNo results\n") 1303 infoShort.append("\nNo results\n") 1304 uLogger.warning("No results. Try changing your search pattern.") 1305 1306 else: 1307 for iType in searchResults: 1308 iTypeValuesCount = len(searchResults[iType].values()) 1309 if iTypeValuesCount > 0: 1310 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1311 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1312 1313 for instrument in searchResults[iType].values(): 1314 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1315 instrument["type"], 1316 instrument["ticker"], 1317 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1318 instrument["figi"], 1319 )) 1320 1321 if iTypeValuesCount <= 5: 1322 infoShort.extend(info[-iTypeValuesCount:]) 1323 1324 else: 1325 infoShort.extend(info[-5:]) 1326 infoShort.append(skippedLine) 1327 1328 infoText = "".join(info) 1329 infoTextShort = "".join(infoShort) 1330 1331 if show: 1332 uLogger.info(infoTextShort) 1333 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1334 1335 if self.searchResultsFile: 1336 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1337 fH.write(infoText) 1338 1339 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1340 1341 return searchResults 1342 1343 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1344 """ 1345 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1346 1347 :param instruments: list of strings with tickers or FIGIs. 1348 :return: list with unique instrument FIGIs only. 1349 """ 1350 requestedInstruments = [] 1351 for iName in instruments: 1352 if iName not in self.aliases.keys(): 1353 if iName not in requestedInstruments: 1354 requestedInstruments.append(iName) 1355 1356 else: 1357 if iName not in requestedInstruments: 1358 if self.aliases[iName] not in requestedInstruments: 1359 requestedInstruments.append(self.aliases[iName]) 1360 1361 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1362 1363 onlyUniqueFIGIs = [] 1364 for iName in requestedInstruments: 1365 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1366 continue 1367 1368 self.ticker = iName 1369 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1370 1371 if not iData: 1372 self.ticker = "" 1373 self.figi = iName 1374 1375 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1376 1377 if not iData: 1378 self.figi = "" 1379 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1380 1381 if iData and iData["figi"] not in onlyUniqueFIGIs: 1382 onlyUniqueFIGIs.append(iData["figi"]) 1383 1384 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1385 1386 return onlyUniqueFIGIs 1387 1388 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1389 """ 1390 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1391 See limits: https://tinkoff.github.io/investAPI/limits/ 1392 If `pricesFile` string is not empty then also save information to this file. 1393 1394 :param instruments: list of strings with tickers or FIGIs. 1395 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1396 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1397 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1398 """ 1399 if instruments is None or not instruments: 1400 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1401 raise Exception("Ticker or FIGI required") 1402 1403 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1404 1405 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1406 1407 iList = [] # trying to get info and current prices about all unique instruments: 1408 for self.figi in onlyUniqueFIGIs: 1409 iData = self.SearchByFIGI(requestPrice=True) 1410 iList.append(iData) 1411 1412 self.ShowListOfPrices(iList, show) 1413 1414 return iList 1415 1416 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1417 """ 1418 Show table contains current prices of given instruments. 1419 1420 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1421 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1422 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1423 :return: multilines text in Markdown format as a table contains current prices. 1424 """ 1425 infoText = "" 1426 1427 if show or self.pricesFile: 1428 info = [ 1429 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1430 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1431 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1432 ] 1433 1434 for item in iList: 1435 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1436 item["ticker"], 1437 item["figi"], 1438 item["type"], 1439 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1440 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1441 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1442 "{} / {}".format( 1443 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1444 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1445 ), 1446 "{} / {}".format( 1447 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1448 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1449 ), 1450 item["currency"], 1451 )) 1452 1453 infoText = "".join(info) 1454 1455 if show: 1456 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1457 1458 if self.pricesFile: 1459 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1460 fH.write(infoText) 1461 1462 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1463 1464 return infoText 1465 1466 def RequestTradingStatus(self) -> dict: 1467 """ 1468 Requesting trading status for the instrument defined by `figi` variable. 1469 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1470 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1471 1472 :return: dictionary with trading status attributes. Response example: 1473 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1474 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1475 """ 1476 if self.figi is None or not self.figi: 1477 uLogger.error("Variable `figi` must be defined for using this method!") 1478 raise Exception("FIGI required") 1479 1480 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1481 1482 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1483 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1484 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1485 1486 uLogger.debug("Records about current trading status successfully received") 1487 1488 return tradingStatus 1489 1490 def RequestPortfolio(self) -> dict: 1491 """ 1492 Requesting actual user's portfolio for current `accountId`. 1493 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1494 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1495 1496 :return: dictionary with user's portfolio. 1497 """ 1498 if self.accountId is None or not self.accountId: 1499 uLogger.error("Variable `accountId` must be defined for using this method!") 1500 raise Exception("Account ID required") 1501 1502 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1503 1504 self.body = str({"accountId": self.accountId}) 1505 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1506 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1507 1508 uLogger.debug("Records about user's portfolio successfully received") 1509 1510 return rawPortfolio 1511 1512 def RequestPositions(self) -> dict: 1513 """ 1514 Requesting open positions by currencies and instruments for current `accountId`. 1515 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1516 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1517 1518 :return: dictionary with open positions by instruments. 1519 """ 1520 if self.accountId is None or not self.accountId: 1521 uLogger.error("Variable `accountId` must be defined for using this method!") 1522 raise Exception("Account ID required") 1523 1524 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1525 1526 self.body = str({"accountId": self.accountId}) 1527 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1528 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1529 1530 uLogger.debug("Records about current open positions successfully received") 1531 1532 return rawPositions 1533 1534 def RequestPendingOrders(self) -> list: 1535 """ 1536 Requesting current actual pending orders for current `accountId`. 1537 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1538 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1539 1540 :return: list of dictionaries with pending orders. 1541 """ 1542 if self.accountId is None or not self.accountId: 1543 uLogger.error("Variable `accountId` must be defined for using this method!") 1544 raise Exception("Account ID required") 1545 1546 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1547 1548 self.body = str({"accountId": self.accountId}) 1549 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1550 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1551 1552 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1553 1554 return rawOrders 1555 1556 def RequestStopOrders(self) -> list: 1557 """ 1558 Requesting current actual stop orders for current `accountId`. 1559 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1560 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1561 1562 :return: list of dictionaries with stop orders. 1563 """ 1564 if self.accountId is None or not self.accountId: 1565 uLogger.error("Variable `accountId` must be defined for using this method!") 1566 raise Exception("Account ID required") 1567 1568 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1569 1570 self.body = str({"accountId": self.accountId}) 1571 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1572 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1573 1574 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1575 1576 return rawStopOrders 1577 1578 def Overview(self, show: bool = False, details: str = "full") -> dict: 1579 """ 1580 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1581 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1582 are defined then also save information to file. 1583 1584 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1585 many requests about the state of the portfolio, and then, based on the received data, a large number 1586 of calculation and statistics are collected. 1587 1588 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1589 :param details: how detailed should the information be? You should specify one of strings: 1590 `full` - shows full available information about portfolio status (by default), 1591 `positions` - shows only open positions, 1592 `digest` - show a short digest of the portfolio status, 1593 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1594 `orders` - shows only sections of open limits and stop orders. 1595 :return: dictionary with client's raw portfolio and some statistics. 1596 """ 1597 if self.accountId is None or not self.accountId: 1598 uLogger.error("Variable `accountId` must be defined for using this method!") 1599 raise Exception("Account ID required") 1600 1601 view = { 1602 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1603 "headers": {}, # list of dictionaries, response headers without "positions" section 1604 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1605 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1606 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1607 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1608 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1609 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1610 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1611 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1612 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1613 }, 1614 "stat": { # --- some statistics calculated using "raw" sections: 1615 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1616 "availableRUB": 0., # available rubles (without other currencies) 1617 "blockedRUB": 0., # blocked sum in Russian Rouble 1618 "totalChangesRUB": 0., # changes for all open trades in RUB 1619 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1620 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1621 "sharesCostRUB": 0., # costs of all shares in RUB 1622 "bondsCostRUB": 0., # costs of all bonds in RUB 1623 "etfsCostRUB": 0., # costs of all etfs in RUB 1624 "futuresCostRUB": 0., # costs of all futures in RUB 1625 "Currencies": [], # list of dictionaries of all currencies statistics 1626 "Shares": [], # list of dictionaries of all shares statistics 1627 "Bonds": [], # list of dictionaries of all bonds statistics 1628 "Etfs": [], # list of dictionaries of all etfs statistics 1629 "Futures": [], # list of dictionaries of all futures statistics 1630 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1631 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1632 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1633 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1634 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1635 }, 1636 "analytics": { # --- some analytics of portfolio: 1637 "distrByAssets": {}, # portfolio distribution by assets 1638 "distrByCompanies": {}, # portfolio distribution by companies 1639 "distrBySectors": {}, # portfolio distribution by sectors 1640 "distrByCurrencies": {}, # portfolio distribution by currencies 1641 "distrByCountries": {}, # portfolio distribution by countries 1642 } 1643 } 1644 1645 details = details.lower() 1646 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1647 if details not in availableDetails: 1648 details = "full" 1649 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1650 1651 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1652 1653 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1654 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1655 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1656 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1657 1658 # save response headers without "positions" section: 1659 for key in portfolioResponse.keys(): 1660 if key != "positions": 1661 view["raw"]["headers"][key] = portfolioResponse[key] 1662 1663 else: 1664 continue 1665 1666 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1667 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1668 for item in portfolioResponse["positions"]: 1669 if item["instrumentType"] == "currency": 1670 self.figi = item["figi"] 1671 curr = self.SearchByFIGI(requestPrice=False) 1672 1673 # current price of currency in RUB: 1674 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1675 "name": curr["name"], 1676 "currentPrice": NanoToFloat( 1677 item["currentPrice"]["units"], 1678 item["currentPrice"]["nano"] 1679 ), 1680 } 1681 1682 view["raw"]["Currencies"].append(item) 1683 1684 elif item["instrumentType"] == "share": 1685 view["raw"]["Shares"].append(item) 1686 1687 elif item["instrumentType"] == "bond": 1688 view["raw"]["Bonds"].append(item) 1689 1690 elif item["instrumentType"] == "etf": 1691 view["raw"]["Etfs"].append(item) 1692 1693 elif item["instrumentType"] == "futures": 1694 view["raw"]["Futures"].append(item) 1695 1696 else: 1697 continue 1698 1699 # how many volume of currencies (by ISO currency name) are blocked: 1700 for item in view["raw"]["positions"]["blocked"]: 1701 blocked = NanoToFloat(item["units"], item["nano"]) 1702 if blocked > 0: 1703 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1704 1705 # how many volume of instruments (by FIGI) are blocked: 1706 for item in view["raw"]["positions"]["securities"]: 1707 blocked = int(item["blocked"]) 1708 if blocked > 0: 1709 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1710 1711 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1712 1713 if "rub" in allBlocked.keys(): 1714 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1715 1716 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1717 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1718 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1719 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1720 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1721 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1722 view["stat"]["portfolioCostRUB"] = sum([ 1723 view["stat"]["allCurrenciesCostRUB"], 1724 view["stat"]["sharesCostRUB"], 1725 view["stat"]["bondsCostRUB"], 1726 view["stat"]["etfsCostRUB"], 1727 view["stat"]["futuresCostRUB"], 1728 ]) 1729 1730 # --- calculating some portfolio statistics: 1731 byComp = {} # distribution by companies 1732 bySect = {} # distribution by sectors 1733 byCurr = {} # distribution by currencies (include RUB) 1734 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1735 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1736 1737 for item in portfolioResponse["positions"]: 1738 self.figi = item["figi"] 1739 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1740 1741 if instrument: 1742 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1743 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1744 1745 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1746 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1747 1748 else: 1749 blocked = 0 1750 1751 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1752 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1753 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1754 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1755 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1756 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1757 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1758 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1759 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1760 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1761 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1762 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1763 1764 statData = { 1765 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1766 "ticker": instrument["ticker"], # ticker by FIGI 1767 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1768 "volume": volume, # available volume of instrument 1769 "lots": lots, # volume in lots of instrument 1770 "direction": direction, # direction of an instrument's position: short or long 1771 "blocked": blocked, # blocked volume of currency or instrument 1772 "currentPrice": curPrice, # current instrument's price in basic asset 1773 "average": average, # current average position price 1774 "cost": cost, # current cost of all volume of instrument in basic asset 1775 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1776 "costRUB": costRUB, # cost of instrument in ruble 1777 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1778 "profit": profit, # expected profit at current moment 1779 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1780 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1781 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1782 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1783 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1784 "step": instrument["step"], # minimum price increment 1785 } 1786 1787 # adding distribution by unique countries: 1788 if statData["country"] not in byCountry.keys(): 1789 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1790 1791 else: 1792 byCountry[statData["country"]]["cost"] += costRUB 1793 byCountry[statData["country"]]["percent"] += percentCostRUB 1794 1795 if item["instrumentType"] != "currency": 1796 # adding distribution by unique companies: 1797 if statData["name"]: 1798 if statData["name"] not in byComp.keys(): 1799 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1800 1801 else: 1802 byComp[statData["name"]]["cost"] += costRUB 1803 byComp[statData["name"]]["percent"] += percentCostRUB 1804 1805 # adding distribution by unique sectors: 1806 if statData["sector"] not in bySect.keys(): 1807 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1808 1809 else: 1810 bySect[statData["sector"]]["cost"] += costRUB 1811 bySect[statData["sector"]]["percent"] += percentCostRUB 1812 1813 # adding distribution by unique currencies: 1814 if currency not in byCurr.keys(): 1815 byCurr[currency] = { 1816 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1817 "cost": costRUB, 1818 "percent": percentCostRUB 1819 } 1820 1821 else: 1822 byCurr[currency]["cost"] += costRUB 1823 byCurr[currency]["percent"] += percentCostRUB 1824 1825 # saving statistics for every instrument: 1826 if item["instrumentType"] == "currency": 1827 view["stat"]["Currencies"].append(statData) 1828 1829 # update dict with free funds for trading (total - blocked) by currencies 1830 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1831 view["stat"]["funds"][currency] = { 1832 "total": volume, 1833 "totalCostRUB": costRUB, # total volume cost in rubles 1834 "free": volume - blocked, 1835 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1836 } 1837 1838 elif item["instrumentType"] == "share": 1839 view["stat"]["Shares"].append(statData) 1840 1841 elif item["instrumentType"] == "bond": 1842 view["stat"]["Bonds"].append(statData) 1843 1844 elif item["instrumentType"] == "etf": 1845 view["stat"]["Etfs"].append(statData) 1846 1847 elif item["instrumentType"] == "Futures": 1848 view["stat"]["Futures"].append(statData) 1849 1850 else: 1851 continue 1852 1853 # total changes in Russian Ruble: 1854 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1855 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1856 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1857 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1858 view["stat"]["funds"]["rub"] = { 1859 "total": view["stat"]["availableRUB"], 1860 "totalCostRUB": view["stat"]["availableRUB"], 1861 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1862 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1863 } 1864 1865 # --- pending orders sector data: 1866 uniquePendingOrders = [] 1867 uniquePendingOrdersFIGIs = [] 1868 for item in view["raw"]["orders"]: 1869 if item["figi"] not in uniquePendingOrdersFIGIs: 1870 uniquePendingOrdersFIGIs.append(item["figi"]) 1871 uniquePendingOrders.append(item) 1872 1873 for item in uniquePendingOrders: 1874 self.figi = item["figi"] 1875 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1876 1877 if instrument: 1878 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1879 orderType = TKS_ORDER_TYPES[item["orderType"]] 1880 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1881 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1882 1883 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1884 if item["direction"] == "ORDER_DIRECTION_BUY": 1885 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1886 1887 else: 1888 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1889 1890 # requested price for order execution: 1891 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1892 1893 # necessary changes in percent to reach target from current price: 1894 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1895 1896 view["stat"]["orders"].append({ 1897 "orderID": item["orderId"], # orderId number parameter of current order 1898 "figi": item["figi"], # FIGI identification 1899 "ticker": instrument["ticker"], # ticker name by FIGI 1900 "lotsRequested": item["lotsRequested"], # requested lots value 1901 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1902 "currentPrice": lastPrice, # current instrument's price for defined action 1903 "targetPrice": target, # requested price for order execution in base currency 1904 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1905 "percentChanges": changes, # changes in percent to target from current price 1906 "currency": item["currency"], # instrument's currency name 1907 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1908 "type": orderType, # type of order from TKS_ORDER_TYPES 1909 "status": orderState, # order status from TKS_ORDER_STATES 1910 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1911 }) 1912 1913 # --- stop orders sector data: 1914 uniqueStopOrders = [] 1915 uniqueStopOrdersFIGIs = [] 1916 for item in view["raw"]["stopOrders"]: 1917 if item["figi"] not in uniqueStopOrdersFIGIs: 1918 uniqueStopOrdersFIGIs.append(item["figi"]) 1919 uniqueStopOrders.append(item) 1920 1921 for item in uniqueStopOrders: 1922 self.figi = item["figi"] 1923 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1924 1925 if instrument: 1926 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1927 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1928 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1929 1930 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1931 if "expirationTime" in item.keys(): 1932 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1933 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1934 1935 else: 1936 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1937 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1938 1939 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1940 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1941 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1942 1943 else: 1944 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1945 1946 # requested price when stop-order executed: 1947 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1948 1949 # price for limit-order, set up when stop-order executed: 1950 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1951 1952 # necessary changes in percent to reach target from current price: 1953 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1954 1955 view["stat"]["stopOrders"].append({ 1956 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1957 "figi": item["figi"], # FIGI identification 1958 "ticker": instrument["ticker"], # ticker name by FIGI 1959 "lotsRequested": item["lotsRequested"], # requested lots value 1960 "currentPrice": lastPrice, # current instrument's price for defined action 1961 "targetPrice": target, # requested price for stop-order execution in base currency 1962 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1963 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1964 "percentChanges": changes, # changes in percent to target from current price 1965 "currency": item["currency"], # instrument's currency name 1966 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1967 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1968 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1969 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1970 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1971 }) 1972 1973 # --- calculating data for analytics section: 1974 # portfolio distribution by assets: 1975 view["analytics"]["distrByAssets"] = { 1976 "Ruble": { 1977 "uniques": 1, 1978 "cost": view["stat"]["availableRUB"], 1979 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1980 }, 1981 "Currencies": { 1982 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1983 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1984 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1985 }, 1986 "Shares": { 1987 "uniques": len(view["stat"]["Shares"]), 1988 "cost": view["stat"]["sharesCostRUB"], 1989 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1990 }, 1991 "Bonds": { 1992 "uniques": len(view["stat"]["Bonds"]), 1993 "cost": view["stat"]["bondsCostRUB"], 1994 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1995 }, 1996 "Etfs": { 1997 "uniques": len(view["stat"]["Etfs"]), 1998 "cost": view["stat"]["etfsCostRUB"], 1999 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2000 }, 2001 "Futures": { 2002 "uniques": len(view["stat"]["Futures"]), 2003 "cost": view["stat"]["futuresCostRUB"], 2004 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2005 }, 2006 } 2007 2008 # portfolio distribution by companies: 2009 view["analytics"]["distrByCompanies"]["All money cash"] = { 2010 "ticker": "", 2011 "cost": view["stat"]["allCurrenciesCostRUB"], 2012 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2013 } 2014 view["analytics"]["distrByCompanies"].update(byComp) 2015 2016 # portfolio distribution by sectors: 2017 view["analytics"]["distrBySectors"]["All money cash"] = { 2018 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2019 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2020 } 2021 view["analytics"]["distrBySectors"].update(bySect) 2022 2023 # portfolio distribution by currencies: 2024 view["analytics"]["distrByCurrencies"].update(byCurr) 2025 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2026 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2027 2028 # portfolio distribution by countries: 2029 view["analytics"]["distrByCountries"].update(byCountry) 2030 2031 # --- Prepare text statistics overview in human-readable: 2032 if show: 2033 # Whatever the value `details`, header not changes: 2034 info = [ 2035 "# Client's portfolio\n\n", 2036 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2037 "* **Account ID:** [{}]\n".format(self.accountId), 2038 ] 2039 2040 if details in ["full", "positions", "digest"]: 2041 info.extend([ 2042 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2043 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2044 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2045 view["stat"]["totalChangesRUB"], 2046 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2047 view["stat"]["totalChangesPercentRUB"], 2048 ), 2049 ]) 2050 2051 if details in ["full", "positions"]: 2052 info.extend([ 2053 "## Open positions\n\n", 2054 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2055 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2056 "| Ruble | {:>31} | | | | | |\n".format( 2057 "{:.2f} ({:.2f}) rub".format( 2058 view["stat"]["availableRUB"], 2059 view["stat"]["blockedRUB"], 2060 ) 2061 ) 2062 ]) 2063 2064 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2065 return [ 2066 "| | | | | | | |\n", 2067 "| {:<27} | | | | | {:>19} | |\n".format( 2068 noTradeStr if noTradeStr else typeStr, 2069 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2070 ), 2071 ] 2072 2073 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2074 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2075 "{} [{}]".format(data["ticker"], data["figi"]), 2076 "{:.2f} ({:.2f}) {}".format( 2077 data["volume"], 2078 data["blocked"], 2079 data["currency"], 2080 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2081 data["volume"], 2082 data["blocked"], 2083 ), 2084 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2085 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2086 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2087 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2088 "{}{:.2f} {} ({}{:.2f}%)".format( 2089 "+" if data["profit"] > 0 else "", 2090 data["profit"], data["baseCurrencyName"], 2091 "+" if data["percentProfit"] > 0 else "", 2092 data["percentProfit"], 2093 ), 2094 ) 2095 2096 # --- Show currencies section: 2097 if view["stat"]["Currencies"]: 2098 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2099 for item in view["stat"]["Currencies"]: 2100 info.append(_InfoStr(item, showCurrencyName=True)) 2101 2102 else: 2103 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2104 2105 # --- Show shares section: 2106 if view["stat"]["Shares"]: 2107 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2108 2109 for item in view["stat"]["Shares"]: 2110 info.append(_InfoStr(item)) 2111 2112 else: 2113 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2114 2115 # --- Show bonds section: 2116 if view["stat"]["Bonds"]: 2117 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2118 2119 for item in view["stat"]["Bonds"]: 2120 info.append(_InfoStr(item)) 2121 2122 else: 2123 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2124 2125 # --- Show etfs section: 2126 if view["stat"]["Etfs"]: 2127 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2128 2129 for item in view["stat"]["Etfs"]: 2130 info.append(_InfoStr(item)) 2131 2132 else: 2133 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2134 2135 # --- Show futures section: 2136 if view["stat"]["Futures"]: 2137 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2138 2139 for item in view["stat"]["Futures"]: 2140 info.append(_InfoStr(item)) 2141 2142 else: 2143 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2144 2145 if details in ["full", "orders"]: 2146 # --- Show pending orders section: 2147 if view["stat"]["orders"]: 2148 info.extend([ 2149 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2150 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2151 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2152 ]) 2153 2154 for item in view["stat"]["orders"]: 2155 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2156 "{} [{}]".format(item["ticker"], item["figi"]), 2157 item["orderID"], 2158 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2159 "{} {} ({}{:.2f}%)".format( 2160 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2161 item["baseCurrencyName"], 2162 "+" if item["percentChanges"] > 0 else "", 2163 float(item["percentChanges"]), 2164 ), 2165 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2166 item["action"], 2167 item["type"], 2168 item["date"], 2169 )) 2170 2171 else: 2172 info.append("\n## Total pending limit-orders: 0\n") 2173 2174 # --- Show stop orders section: 2175 if view["stat"]["stopOrders"]: 2176 info.extend([ 2177 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2178 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2179 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2180 ]) 2181 2182 for item in view["stat"]["stopOrders"]: 2183 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2184 "{} [{}]".format(item["ticker"], item["figi"]), 2185 item["orderID"], 2186 item["lotsRequested"], 2187 "{} {} ({}{:.2f}%)".format( 2188 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2189 item["baseCurrencyName"], 2190 "+" if item["percentChanges"] > 0 else "", 2191 float(item["percentChanges"]), 2192 ), 2193 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2194 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2195 item["action"], 2196 item["type"], 2197 item["expType"], 2198 item["createDate"], 2199 item["expDate"], 2200 )) 2201 2202 else: 2203 info.append("\n## Total stop-orders: 0\n") 2204 2205 if details in ["full", "analytics"]: 2206 # -- Show analytics section: 2207 if view["stat"]["portfolioCostRUB"] > 0: 2208 info.extend([ 2209 "\n# Analytics\n" 2210 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2211 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2212 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2213 view["stat"]["totalChangesRUB"], 2214 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2215 view["stat"]["totalChangesPercentRUB"], 2216 ), 2217 "\n## Portfolio distribution by assets\n" 2218 "\n| Type | Uniques | Percent | Current cost |\n", 2219 "|------------|---------|---------|--------------------|\n", 2220 ]) 2221 2222 for key in view["analytics"]["distrByAssets"].keys(): 2223 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2224 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2225 key, 2226 view["analytics"]["distrByAssets"][key]["uniques"], 2227 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2228 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2229 )) 2230 2231 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2232 info.extend([ 2233 "\n## Portfolio distribution by companies\n" 2234 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2235 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2236 ]) 2237 2238 for company in view["analytics"]["distrByCompanies"].keys(): 2239 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2240 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2241 info.append("| {} | {:<7} | {:<18} |\n".format( 2242 "{}{}{}".format( 2243 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2244 company, 2245 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2246 ), 2247 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2248 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2249 )) 2250 2251 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2252 info.extend([ 2253 "\n## Portfolio distribution by sectors\n" 2254 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2255 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2256 ]) 2257 2258 for sector in view["analytics"]["distrBySectors"].keys(): 2259 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2260 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2261 sector, 2262 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2263 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2264 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2265 )) 2266 2267 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2268 info.extend([ 2269 "\n## Portfolio distribution by currencies\n" 2270 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2271 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2272 ]) 2273 2274 for curr in view["analytics"]["distrByCurrencies"].keys(): 2275 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2276 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2277 info.append("| {} | {:<7} | {:<18} |\n".format( 2278 "[{}] {}{}".format( 2279 curr, 2280 view["analytics"]["distrByCurrencies"][curr]["name"], 2281 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2282 ), 2283 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2284 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2285 )) 2286 2287 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2288 info.extend([ 2289 "\n## Portfolio distribution by countries\n" 2290 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2291 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2292 ]) 2293 2294 for country in view["analytics"]["distrByCountries"].keys(): 2295 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2296 nameLen = len(country) 2297 info.append("| {} | {:<7} | {:<18} |\n".format( 2298 "{}{}".format( 2299 country, 2300 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2301 ), 2302 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2303 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2304 )) 2305 2306 infoText = "".join(info) 2307 2308 uLogger.info(infoText) 2309 2310 if details == "full" and self.overviewFile: 2311 filename = self.overviewFile 2312 2313 elif details == "digest" and self.overviewDigestFile: 2314 filename = self.overviewDigestFile 2315 2316 elif details == "positions" and self.overviewPositionsFile: 2317 filename = self.overviewPositionsFile 2318 2319 elif details == "orders" and self.overviewOrdersFile: 2320 filename = self.overviewOrdersFile 2321 2322 elif details == "analytics" and self.overviewAnalyticsFile: 2323 filename = self.overviewAnalyticsFile 2324 2325 else: 2326 filename = "" 2327 2328 if filename: 2329 with open(filename, "w", encoding="UTF-8") as fH: 2330 fH.write(infoText) 2331 2332 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2333 2334 return view 2335 2336 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2337 """ 2338 Returns history operations between two given dates for current `accountId`. 2339 If `reportFile` string is not empty then also save human-readable report. 2340 Shows some statistical data of closed positions. 2341 2342 :param start: see docstring in `GetDatesAsString()` method 2343 :param end: see docstring in `GetDatesAsString()` method 2344 :param show: if `True` then also prints all records to the console. 2345 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2346 :return: original list of dictionaries with history of deals records from API ("operations" key): 2347 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2348 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2349 """ 2350 if self.accountId is None or not self.accountId: 2351 uLogger.error("Variable `accountId` must be defined for using this method!") 2352 raise Exception("Account ID required") 2353 2354 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2355 2356 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2357 2358 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2359 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2360 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2361 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2362 customStat = {} # custom statistics in additional to responseJSON 2363 2364 # --- output report in human-readable format: 2365 if show or self.reportFile: 2366 splitLine1 = "| | | | | |\n" # Summary section 2367 splitLine2 = "| | | | | | | | |\n" # Operations section 2368 nextDay = "" 2369 2370 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2371 2372 if len(ops) > 0: 2373 customStat = { 2374 "opsCount": 0, # total operations count 2375 "buyCount": 0, # buy operations 2376 "sellCount": 0, # sell operations 2377 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2378 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2379 "payIn": {"rub": 0.}, # Deposit brokerage account 2380 "payOut": {"rub": 0.}, # Withdrawals 2381 "divs": {"rub": 0.}, # Dividends income 2382 "coupons": {"rub": 0.}, # Coupon's income 2383 "brokerCom": {"rub": 0.}, # Service commissions 2384 "serviceCom": {"rub": 0.}, # Service commissions 2385 "marginCom": {"rub": 0.}, # Margin commissions 2386 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2387 } 2388 2389 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2390 for item in ops: 2391 if item["state"] == "OPERATION_STATE_EXECUTED": 2392 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2393 2394 # count buy operations: 2395 if "_BUY" in item["operationType"]: 2396 customStat["buyCount"] += 1 2397 2398 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2399 customStat["buyTotal"][item["payment"]["currency"]] += payment 2400 2401 else: 2402 customStat["buyTotal"][item["payment"]["currency"]] = payment 2403 2404 # count sell operations: 2405 elif "_SELL" in item["operationType"]: 2406 customStat["sellCount"] += 1 2407 2408 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2409 customStat["sellTotal"][item["payment"]["currency"]] += payment 2410 2411 else: 2412 customStat["sellTotal"][item["payment"]["currency"]] = payment 2413 2414 # count incoming operations: 2415 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2416 if item["payment"]["currency"] in customStat["payIn"].keys(): 2417 customStat["payIn"][item["payment"]["currency"]] += payment 2418 2419 else: 2420 customStat["payIn"][item["payment"]["currency"]] = payment 2421 2422 # count withdrawals operations: 2423 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2424 if item["payment"]["currency"] in customStat["payOut"].keys(): 2425 customStat["payOut"][item["payment"]["currency"]] += payment 2426 2427 else: 2428 customStat["payOut"][item["payment"]["currency"]] = payment 2429 2430 # count dividends income: 2431 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2432 if item["payment"]["currency"] in customStat["divs"].keys(): 2433 customStat["divs"][item["payment"]["currency"]] += payment 2434 2435 else: 2436 customStat["divs"][item["payment"]["currency"]] = payment 2437 2438 # count coupon's income: 2439 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2440 if item["payment"]["currency"] in customStat["coupons"].keys(): 2441 customStat["coupons"][item["payment"]["currency"]] += payment 2442 2443 else: 2444 customStat["coupons"][item["payment"]["currency"]] = payment 2445 2446 # count broker commissions: 2447 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2448 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2449 customStat["brokerCom"][item["payment"]["currency"]] += payment 2450 2451 else: 2452 customStat["brokerCom"][item["payment"]["currency"]] = payment 2453 2454 # count service commissions: 2455 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2456 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2457 customStat["serviceCom"][item["payment"]["currency"]] += payment 2458 2459 else: 2460 customStat["serviceCom"][item["payment"]["currency"]] = payment 2461 2462 # count margin commissions: 2463 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2464 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2465 customStat["marginCom"][item["payment"]["currency"]] += payment 2466 2467 else: 2468 customStat["marginCom"][item["payment"]["currency"]] = payment 2469 2470 # count withholding taxes: 2471 elif "_TAX" in item["operationType"]: 2472 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2473 customStat["allTaxes"][item["payment"]["currency"]] += payment 2474 2475 else: 2476 customStat["allTaxes"][item["payment"]["currency"]] = payment 2477 2478 else: 2479 continue 2480 2481 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2482 2483 # --- view "Actions" lines: 2484 info.extend([ 2485 "| 1 | 2 | 3 | 4 | 5 |\n", 2486 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2487 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2488 "| | Buy: {:<22} | {:<28} | | |\n".format( 2489 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2490 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2491 ), 2492 "| | Sell: {:<21} | {:<28} | | |\n".format( 2493 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2494 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2495 ), 2496 ]) 2497 2498 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2499 for key in opsKeys: 2500 if key == "rub": 2501 continue 2502 2503 info.extend([ 2504 "| | | {:<28} | | |\n".format( 2505 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2506 ), 2507 "| | | {:<28} | | |\n".format( 2508 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2509 ), 2510 ]) 2511 2512 info.append(splitLine1) 2513 2514 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2515 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2516 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2517 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2518 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2519 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2520 ) 2521 2522 # --- view "Payments" lines: 2523 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2524 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2525 2526 for key in paymentsKeys: 2527 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2528 2529 info.append(splitLine1) 2530 2531 # --- view "Commissions and taxes" lines: 2532 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2533 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2534 2535 for key in comKeys: 2536 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2537 2538 info.append(splitLine1) 2539 2540 info.extend([ 2541 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2542 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2543 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2544 ]) 2545 2546 else: 2547 info.append("Broker returned no operations during this period\n") 2548 2549 # --- view "Operations" section: 2550 for item in ops: 2551 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2552 continue 2553 2554 else: 2555 self.figi = item["figi"] if item["figi"] else "" 2556 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2557 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2558 2559 # group of deals during one day: 2560 if nextDay and item["date"].split("T")[0] != nextDay: 2561 info.append(splitLine2) 2562 nextDay = "" 2563 2564 else: 2565 nextDay = item["date"].split("T")[0] # saving current day for splitting 2566 2567 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2568 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2569 self.figi if self.figi else "—", 2570 instrument["ticker"] if instrument else "—", 2571 instrument["type"] if instrument else "—", 2572 item["quantity"] if int(item["quantity"]) > 0 else "—", 2573 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2574 TKS_OPERATION_STATES[item["state"]], 2575 TKS_OPERATION_TYPES[item["operationType"]], 2576 )) 2577 2578 infoText = "".join(info) 2579 2580 if show: 2581 uLogger.info(infoText) 2582 2583 if self.reportFile: 2584 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2585 fH.write(infoText) 2586 2587 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2588 2589 return ops, customStat 2590 2591 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2592 """ 2593 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2594 2595 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2596 Warning! Broker server used ISO UTC time by default. 2597 2598 If `historyFile` is not `None` then method save history to file, otherwise return only pandas dataframe. 2599 Also, `historyFile` used to update history with `onlyMissing` parameter. 2600 2601 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2602 2603 :param start: see docstring in `GetDatesAsString()` method. 2604 :param end: see docstring in `GetDatesAsString()` method. 2605 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2606 `"hour"`, `"day"`. Default: `"hour"`. 2607 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2608 False by default. Warning! History appends only from last candle to current time 2609 with always update last candle! 2610 :param csvSep: separator if csv-file is used, `,` by default. 2611 :param show: if `True` then also prints pandas dataframe to the console. 2612 :return: pandas dataframe with prices history. Headers of columns are defined by default: 2613 `["date", "time", "open", "high", "low", "close", "volume"]`. 2614 """ 2615 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2616 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2617 history = None # empty pandas object for history 2618 2619 if interval not in TKS_CANDLE_INTERVALS.keys(): 2620 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2621 raise Exception("Incorrect value") 2622 2623 if not (self.ticker or self.figi): 2624 uLogger.error("Ticker or FIGI must be defined!") 2625 raise Exception("Ticker or FIGI required") 2626 2627 if self.ticker and not self.figi: 2628 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2629 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2630 2631 if self.figi and not self.ticker: 2632 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2633 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2634 2635 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2636 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2637 if interval.lower() != "day": 2638 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2639 2640 delta = dtEnd - dtStart # current UTC time minus last time in file 2641 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2642 2643 # calculate history length in candles: 2644 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2645 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2646 length += 1 # to avoid fraction time 2647 2648 # calculate data blocks count: 2649 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2650 2651 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2652 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2653 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2654 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2655 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2656 2657 tempOld = None # pandas object for old history, if --only-missing key present 2658 lastTime = None # datetime object of last old candle in file 2659 2660 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2661 uLogger.debug("--only-missing key present, add only last missing candles...") 2662 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2663 2664 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2665 2666 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2667 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2668 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2669 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2670 2671 # get last datetime object from last string in file or minus 1 delta if file is empty: 2672 if len(tempOld) > 0: 2673 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2674 2675 else: 2676 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2677 2678 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2679 2680 responseJSONs = [] # raw history blocks of data 2681 2682 blockEnd = dtEnd 2683 for item in range(blocks): 2684 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2685 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2686 2687 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2688 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2689 )) 2690 2691 if blockStart == blockEnd: 2692 uLogger.debug("Skipped this zero-length block...") 2693 2694 else: 2695 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2696 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2697 self.body = str({ 2698 "figi": self.figi, 2699 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2700 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2701 "interval": TKS_CANDLE_INTERVALS[interval][0] 2702 }) 2703 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2704 2705 if "code" in responseJSON.keys(): 2706 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2707 2708 else: 2709 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2710 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2711 2712 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2713 2714 blockEnd = blockStart 2715 2716 printCount = len(responseJSONs) # candles to show in console 2717 if responseJSONs: 2718 tempHistory = pd.DataFrame( 2719 data={ 2720 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2721 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2722 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2723 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2724 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2725 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2726 "volume": [int(item["volume"]) for item in responseJSONs], 2727 }, 2728 index=range(len(responseJSONs)), 2729 columns=["date", "time", "open", "high", "low", "close", "volume"], 2730 ) 2731 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2732 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2733 2734 # append only newest candles to old history if --only-missing key present: 2735 if onlyMissing and tempOld is not None and lastTime is not None: 2736 index = 0 # find start index in tempHistory data: 2737 2738 for i, item in tempHistory.iterrows(): 2739 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2740 2741 if curTime == lastTime: 2742 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2743 index = i 2744 printCount = index + 1 2745 break 2746 2747 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2748 2749 else: 2750 history = tempHistory # if no `--only-missing` key then load full data from server 2751 2752 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2753 2754 if history is not None and not history.empty: 2755 if show: 2756 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2757 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2758 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2759 )) 2760 2761 else: 2762 uLogger.warning("Received an empty candles history!") 2763 2764 if self.historyFile is not None: 2765 if history is not None and not history.empty: 2766 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2767 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2768 2769 else: 2770 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2771 2772 else: 2773 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only pandas dataframe returns.") 2774 2775 return history 2776 2777 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2778 """ 2779 Load candles history from csv-file and return pandas dataframe object. 2780 2781 See also: `History()` and `ShowHistoryChart()` methods. 2782 2783 :param filePath: path to csv-file to open. 2784 """ 2785 loadedHistory = None # init candles data object 2786 2787 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2788 2789 if os.path.exists(filePath): 2790 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as pandas dataframe 2791 2792 tfStr = self.priceModel.FormattedDelta( 2793 self.priceModel.timeframe, 2794 "{days} days {hours}h {minutes}m {seconds}s", 2795 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2796 self.priceModel.timeframe, 2797 "{hours}h {minutes}m {seconds}s", 2798 ) 2799 2800 if loadedHistory is not None and not loadedHistory.empty: 2801 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2802 len(loadedHistory), 2803 tfStr, 2804 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2805 ) 2806 2807 else: 2808 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2809 2810 else: 2811 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2812 2813 return loadedHistory 2814 2815 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2816 """ 2817 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2818 2819 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2820 Default: `index.html` (both for interact and non-interact candlesticks chart). 2821 2822 See also: `History()` and `LoadHistory()` methods. 2823 2824 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2825 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2826 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2827 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2828 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2829 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2830 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2831 """ 2832 if isinstance(candles, str): 2833 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2834 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2835 2836 elif isinstance(candles, pd.DataFrame): 2837 self.priceModel.prices = candles # set candles chain from variable 2838 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2839 2840 if "datetime" not in candles.columns: 2841 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2842 2843 else: 2844 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2845 raise Exception("Incorrect value") 2846 2847 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2848 2849 if interact: 2850 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2851 2852 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2853 2854 else: 2855 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2856 2857 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2858 2859 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2860 2861 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2862 """ 2863 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2864 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2865 2866 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2867 2868 :param operation: string "Buy" or "Sell". 2869 :param lots: volume, integer count of lots >= 1. 2870 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2871 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2872 :param expDate: string "Undefined" by default or local date in future, 2873 it is a string with format `%Y-%m-%d %H:%M:%S`. 2874 :return: JSON with response from broker server. 2875 """ 2876 if self.accountId is None or not self.accountId: 2877 uLogger.error("Variable `accountId` must be defined for using this method!") 2878 raise Exception("Account ID required") 2879 2880 if operation is None or not operation or operation not in ("Buy", "Sell"): 2881 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2882 raise Exception("Incorrect value") 2883 2884 if lots is None or lots < 1: 2885 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2886 lots = 1 2887 2888 if tp is None or tp < 0: 2889 tp = 0 2890 2891 if sl is None or sl < 0: 2892 sl = 0 2893 2894 if expDate is None or not expDate: 2895 expDate = "Undefined" 2896 2897 if not (self.ticker or self.figi): 2898 uLogger.error("Ticker or FIGI must be defined!") 2899 raise Exception("Ticker or FIGI required") 2900 2901 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2902 self.ticker = instrument["ticker"] 2903 self.figi = instrument["figi"] 2904 2905 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2906 2907 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2908 self.body = str({ 2909 "figi": self.figi, 2910 "quantity": str(lots), 2911 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2912 "accountId": str(self.accountId), 2913 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2914 }) 2915 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2916 2917 if "orderId" in response.keys(): 2918 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2919 operation, response["orderId"], 2920 self.ticker, self.figi, lots, 2921 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2922 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2923 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2924 )) 2925 2926 else: 2927 uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.") 2928 2929 if tp > 0: 2930 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2931 2932 if sl > 0: 2933 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2934 2935 return response 2936 2937 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2938 """ 2939 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2940 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2941 2942 See also: `Order()` and `Trade()` docstrings. 2943 2944 :param lots: volume, integer count of lots >= 1. 2945 :param tp: float > 0, take profit price of stop-order. 2946 :param sl: float > 0, stop loss price of stop-order. 2947 :param expDate: it's a local date in future. 2948 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2949 :return: JSON with response from broker server. 2950 """ 2951 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2952 2953 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2954 """ 2955 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2956 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2957 2958 See also: `Order()` and `Trade()` docstrings. 2959 2960 :param lots: volume, integer count of lots >= 1. 2961 :param tp: float > 0, take profit price of stop-order. 2962 :param sl: float > 0, stop loss price of stop-order. 2963 :param expDate: it's a local date in the future. 2964 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2965 :return: JSON with response from broker server. 2966 """ 2967 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 2968 2969 def CloseTrades(self, tickers: list, portfolio: dict = None) -> None: 2970 """ 2971 Close position of given instruments. 2972 2973 :param tickers: tickers list of instruments that must be closed. 2974 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2975 This avoids unnecessary downloading data from the server. 2976 """ 2977 if not tickers: 2978 uLogger.info("Tickers list is empty, nothing to close.") 2979 2980 else: 2981 if portfolio is None or not portfolio: 2982 portfolio = self.Overview(show=False) 2983 2984 allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2985 uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers)) 2986 2987 for ticker in tickers: 2988 if ticker not in allOpenedTickers: 2989 uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker)) 2990 continue 2991 2992 # search open trade info about instrument by ticker: 2993 instrument = {} 2994 for iType in TKS_INSTRUMENTS: 2995 if instrument: 2996 break 2997 2998 for item in portfolio["stat"][iType]: 2999 if item["ticker"] == ticker: 3000 instrument = item 3001 break 3002 3003 if instrument: 3004 self.ticker = ticker 3005 self.figi = instrument["figi"] 3006 3007 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3008 self.ticker, 3009 self.figi, 3010 int(instrument["volume"]), 3011 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3012 )) 3013 3014 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3015 3016 if tradeLots > 0: 3017 if instrument["blocked"] > 0: 3018 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3019 instrument["blocked"], 3020 self.ticker, 3021 tradeLots, 3022 )) 3023 3024 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3025 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3026 3027 else: 3028 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3029 3030 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3031 """ 3032 Close all positions of given instruments with defined type. 3033 3034 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3035 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3036 This avoids unnecessary downloading data from the server. 3037 """ 3038 if iType not in TKS_INSTRUMENTS: 3039 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3040 3041 else: 3042 if portfolio is None or not portfolio: 3043 portfolio = self.Overview(show=False) 3044 3045 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3046 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3047 3048 if tickers and portfolio: 3049 self.CloseTrades(tickers, portfolio) 3050 3051 else: 3052 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3053 3054 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3055 """ 3056 Universal method to create market or limit orders with all available parameters for current `accountId`. 3057 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3058 3059 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3060 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3061 3062 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3063 then broker immediately open market order as you can do simple --buy or --sell operations! 3064 3065 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3066 When current price will go up or down to target price value then broker opens a limit order. 3067 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3068 3069 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3070 3071 :param operation: string "Buy" or "Sell". 3072 :param orderType: string "Limit" or "Stop". 3073 :param lots: volume, integer count of lots >= 1. 3074 :param targetPrice: target price > 0. This is open trade price for limit order. 3075 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3076 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3077 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3078 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3079 Stop loss order always executed by market price. 3080 :param expDate: string "Undefined" by default or local date in future. 3081 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3082 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3083 A limit order has no expiration date, it lasts until the end of the trading day. 3084 :return: JSON with response from broker server. 3085 """ 3086 if self.accountId is None or not self.accountId: 3087 uLogger.error("Variable `accountId` must be defined for using this method!") 3088 raise Exception("Account ID required") 3089 3090 if operation is None or not operation or operation not in ("Buy", "Sell"): 3091 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3092 raise Exception("Incorrect value") 3093 3094 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3095 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3096 raise Exception("Incorrect value") 3097 3098 if lots is None or lots < 1: 3099 uLogger.error("You must define trade volume > 0: integer count of lots!") 3100 raise Exception("Incorrect value") 3101 3102 if targetPrice is None or targetPrice <= 0: 3103 uLogger.error("Target price for limit-order must be greater than 0!") 3104 raise Exception("Incorrect value") 3105 3106 if limitPrice is None or limitPrice <= 0: 3107 limitPrice = targetPrice 3108 3109 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3110 stopType = "Limit" 3111 3112 if expDate is None or not expDate: 3113 expDate = "Undefined" 3114 3115 if not (self.ticker or self.figi): 3116 uLogger.error("Tocker or FIGI must be defined!") 3117 raise Exception("Ticker or FIGI required") 3118 3119 response = {} 3120 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3121 self.ticker = instrument["ticker"] 3122 self.figi = instrument["figi"] 3123 3124 if orderType == "Limit": 3125 uLogger.debug( 3126 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3127 self.ticker, self.figi, 3128 operation, lots, targetPrice, instrument["currency"], 3129 )) 3130 3131 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3132 self.body = str({ 3133 "figi": self.figi, 3134 "quantity": str(lots), 3135 "price": FloatToNano(targetPrice), 3136 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3137 "accountId": str(self.accountId), 3138 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3139 }) 3140 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3141 3142 if "orderId" in response.keys(): 3143 uLogger.info( 3144 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3145 response["orderId"], 3146 self.ticker, self.figi, 3147 operation, lots, targetPrice, instrument["currency"], 3148 )) 3149 3150 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3151 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3152 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3153 targetPrice, instrument["currency"], 3154 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3155 )) 3156 3157 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3158 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3159 targetPrice, instrument["currency"], 3160 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3161 )) 3162 3163 else: 3164 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3165 3166 if orderType == "Stop": 3167 uLogger.debug( 3168 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3169 self.ticker, self.figi, 3170 operation, lots, 3171 targetPrice, instrument["currency"], 3172 limitPrice, instrument["currency"], 3173 stopType, expDate, 3174 )) 3175 3176 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3177 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3178 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3179 3180 body = { 3181 "figi": self.figi, 3182 "quantity": str(lots), 3183 "price": FloatToNano(limitPrice), 3184 "stopPrice": FloatToNano(targetPrice), 3185 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3186 "accountId": str(self.accountId), 3187 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3188 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3189 } 3190 3191 if expDateUTC: 3192 body["expireDate"] = expDateUTC 3193 3194 self.body = str(body) 3195 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3196 3197 if "stopOrderId" in response.keys(): 3198 uLogger.info( 3199 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3200 response["stopOrderId"], 3201 self.ticker, self.figi, 3202 operation, lots, 3203 targetPrice, instrument["currency"], 3204 limitPrice, instrument["currency"], 3205 TKS_STOP_ORDER_TYPES[stopOrderType], 3206 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3207 )) 3208 3209 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3210 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3211 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3212 targetPrice, instrument["currency"], 3213 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3214 )) 3215 3216 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3217 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3218 targetPrice, instrument["currency"], 3219 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3220 )) 3221 3222 else: 3223 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3224 3225 return response 3226 3227 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3228 """ 3229 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3230 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3231 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3232 See also: `Order()` docstring. 3233 3234 :param lots: volume, integer count of lots >= 1. 3235 :param targetPrice: target price > 0. This is open trade price for limit order. 3236 :return: JSON with response from broker server. 3237 """ 3238 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3239 3240 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3241 """ 3242 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3243 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3244 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3245 target price value then broker opens a limit order. See also: `Order()` docstring. 3246 3247 :param lots: volume, integer count of lots >= 1. 3248 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3249 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3250 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3251 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3252 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3253 :param expDate: string "Undefined" by default or local date in future. 3254 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3255 This date is converting to UTC format for server. 3256 :return: JSON with response from broker server. 3257 """ 3258 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3259 3260 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3261 """ 3262 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3263 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3264 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3265 See also: `Order()` docstring. 3266 3267 :param lots: volume, integer count of lots >= 1. 3268 :param targetPrice: target price > 0. This is open trade price for limit order. 3269 :return: JSON with response from broker server. 3270 """ 3271 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3272 3273 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3274 """ 3275 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3276 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3277 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3278 target price value then broker opens a limit order. See also: `Order()` docstring. 3279 3280 :param lots: volume, integer count of lots >= 1. 3281 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3282 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3283 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3284 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3285 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3286 :param expDate: string "Undefined" by default or local date in future. 3287 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3288 This date is converting to UTC format for server. 3289 :return: JSON with response from broker server. 3290 """ 3291 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3292 3293 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3294 """ 3295 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3296 3297 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3298 :param allOrdersIDs: pre-received lists of all active pending orders. 3299 This avoids unnecessary downloading data from the server. 3300 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3301 """ 3302 if self.accountId is None or not self.accountId: 3303 uLogger.error("Variable `accountId` must be defined for using this method!") 3304 raise Exception("Account ID required") 3305 3306 if orderIDs: 3307 if allOrdersIDs is None or not allOrdersIDs: 3308 rawOrders = self.RequestPendingOrders() 3309 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3310 3311 if allStopOrdersIDs is None or not allStopOrdersIDs: 3312 rawStopOrders = self.RequestStopOrders() 3313 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3314 3315 for orderID in orderIDs: 3316 idInPendingOrders = orderID in allOrdersIDs 3317 idInStopOrders = orderID in allStopOrdersIDs 3318 3319 if not (idInPendingOrders or idInStopOrders): 3320 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3321 continue 3322 3323 else: 3324 if idInPendingOrders: 3325 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3326 3327 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3328 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3329 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3330 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3331 3332 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3333 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3334 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3335 3336 else: 3337 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3338 3339 elif idInStopOrders: 3340 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3341 3342 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3343 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3344 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3345 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3346 3347 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3348 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3349 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3350 3351 else: 3352 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3353 3354 else: 3355 continue 3356 3357 def CloseAllOrders(self) -> None: 3358 """ 3359 Gets a list of open pending and stop orders and cancel it all. 3360 """ 3361 rawOrders = self.RequestPendingOrders() 3362 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3363 lenOrders = len(allOrdersIDs) 3364 3365 rawStopOrders = self.RequestStopOrders() 3366 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3367 lenSOrders = len(allStopOrdersIDs) 3368 3369 if lenOrders > 0 or lenSOrders > 0: 3370 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3371 3372 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3373 3374 else: 3375 uLogger.info("Orders not found, nothing to cancel.") 3376 3377 def CloseAll(self, *args) -> None: 3378 """ 3379 Close all available (not blocked) opened trades and orders. 3380 3381 Also, you can select one or more keywords case-insensitive: 3382 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3383 3384 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3385 """ 3386 overview = self.Overview(show=False) # get all open trades info 3387 3388 if len(args) == 0: 3389 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3390 self.CloseAllOrders() # close all pending and stop orders 3391 3392 for iType in TKS_INSTRUMENTS: 3393 if iType != "Currencies": 3394 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3395 3396 else: 3397 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3398 lowerArgs = [x.lower() for x in args] 3399 3400 if "orders" in lowerArgs: 3401 self.CloseAllOrders() # close all pending and stop orders 3402 3403 for iType in TKS_INSTRUMENTS: 3404 if iType.lower() in lowerArgs and iType != "Currencies": 3405 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3406 3407 @staticmethod 3408 def ParseOrderParameters(operation, **inputParameters): 3409 """ 3410 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3411 3412 :param operation: string "Buy" or "Sell". 3413 :param inputParameters: this is dict of strings that looks like this 3414 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3415 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3416 "prices" key: one or more prices to open limit-orders 3417 Counts of values in lots and prices lists must be equals! 3418 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3419 """ 3420 # TODO: update order grid work with api v2 3421 pass 3422 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3423 # 3424 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3425 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3426 # raise Exception("Incorrect value") 3427 # 3428 # if "l" in inputParameters.keys(): 3429 # inputParameters["lots"] = inputParameters.pop("l") 3430 # 3431 # if "p" in inputParameters.keys(): 3432 # inputParameters["prices"] = inputParameters.pop("p") 3433 # 3434 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3435 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3436 # raise Exception("Incorrect value") 3437 # 3438 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3439 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3440 # 3441 # if len(lots) != len(prices): 3442 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3443 # raise Exception("Incorrect value") 3444 # 3445 # uLogger.debug("Extracted parameters for orders:") 3446 # uLogger.debug("lots = {}".format(lots)) 3447 # uLogger.debug("prices = {}".format(prices)) 3448 # 3449 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3450 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3451 # uLogger.debug("Order parameters: {}".format(result)) 3452 # 3453 # return result 3454 3455 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3456 """ 3457 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3458 3459 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3460 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3461 """ 3462 result = False 3463 msg = "Instrument not defined!" 3464 3465 if portfolio is None or not portfolio: 3466 portfolio = self.Overview(show=False) 3467 3468 if self.ticker: 3469 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3470 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3471 3472 for iType in TKS_INSTRUMENTS: 3473 for instrument in portfolio["stat"][iType]: 3474 if instrument["ticker"] == self.ticker: 3475 result = True 3476 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3477 break 3478 3479 elif self.figi: 3480 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3481 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3482 3483 for iType in TKS_INSTRUMENTS: 3484 for instrument in portfolio["stat"][iType]: 3485 if instrument["figi"] == self.figi: 3486 result = True 3487 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3488 break 3489 3490 else: 3491 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3492 3493 uLogger.debug(msg) 3494 3495 return result 3496 3497 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3498 """ 3499 Returns instrument is in the user's portfolio if it presents there. 3500 Instrument must be defined by `ticker` (highly priority) or `figi`. 3501 3502 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3503 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3504 """ 3505 result = None 3506 msg = "Instrument not defined!" 3507 3508 if portfolio is None or not portfolio: 3509 portfolio = self.Overview(show=False) 3510 3511 if self.ticker: 3512 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3513 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3514 3515 for iType in TKS_INSTRUMENTS: 3516 for instrument in portfolio["stat"][iType]: 3517 if instrument["ticker"] == self.ticker: 3518 result = instrument 3519 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3520 break 3521 3522 elif self.figi: 3523 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3524 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3525 3526 for iType in TKS_INSTRUMENTS: 3527 for instrument in portfolio["stat"][iType]: 3528 if instrument["figi"] == self.figi: 3529 result = instrument 3530 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3531 break 3532 3533 else: 3534 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3535 3536 uLogger.debug(msg) 3537 3538 return result 3539 3540 def RequestLimits(self) -> dict: 3541 """ 3542 Method for obtaining the available funds for withdrawal for current `accountId`. 3543 3544 See also: 3545 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3546 - `OverviewLimits()` method 3547 3548 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3549 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3550 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3551 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3552 """ 3553 if self.accountId is None or not self.accountId: 3554 uLogger.error("Variable `accountId` must be defined for using this method!") 3555 raise Exception("Account ID required") 3556 3557 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3558 3559 self.body = str({"accountId": self.accountId}) 3560 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3561 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3562 3563 uLogger.debug("Records about available funds for withdrawal successfully received") 3564 3565 return rawLimits 3566 3567 def OverviewLimits(self, show: bool = False) -> dict: 3568 """ 3569 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3570 3571 See also: `RequestLimits()`. 3572 3573 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3574 :return: dict with raw parsed data from server and some calculated statistics about it. 3575 """ 3576 if self.accountId is None or not self.accountId: 3577 uLogger.error("Variable `accountId` must be defined for using this method!") 3578 raise Exception("Account ID required") 3579 3580 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3581 3582 view = { 3583 "rawLimits": rawLimits, 3584 "limits": { # parsed data for every currency: 3585 "money": { # this is an array of portfolio currency positions 3586 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3587 }, 3588 "blocked": { # this is an array of blocked currency 3589 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3590 }, 3591 "blockedGuarantee": { # this is locked money under collateral for futures 3592 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3593 }, 3594 }, 3595 } 3596 3597 # --- Prepare text table with limits in human-readable format: 3598 if show: 3599 info = [ 3600 "# Withdrawal limits\n\n", 3601 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3602 "* **Account ID:** [{}]\n".format(self.accountId), 3603 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3604 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3605 ] 3606 3607 for curr in view["limits"]["money"].keys(): 3608 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3609 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3610 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3611 3612 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3613 "[{}]".format(curr), 3614 "{:.2f}".format(view["limits"]["money"][curr]), 3615 "{:.2f}".format(availableMoney), 3616 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3617 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3618 ) 3619 3620 if curr == "rub": 3621 info.insert(5, infoStr) # insert at first position in table and after headers 3622 3623 else: 3624 info.append(infoStr) 3625 3626 infoText = "".join(info) 3627 3628 uLogger.info(infoText) 3629 3630 if self.withdrawalLimitsFile: 3631 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3632 fH.write(infoText) 3633 3634 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3635 3636 return view 3637 3638 def RequestAccounts(self) -> dict: 3639 """ 3640 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3641 3642 See also: 3643 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3644 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3645 - `OverviewUserInfo()` method 3646 3647 :return: dict with raw data from server that contains accounts info. Example of dict: 3648 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3649 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3650 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3651 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3652 """ 3653 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3654 3655 self.body = str({}) 3656 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3657 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3658 3659 uLogger.debug("Records about available accounts successfully received") 3660 3661 return rawAccounts 3662 3663 def RequestUserInfo(self) -> dict: 3664 """ 3665 Method for requesting common user's information. 3666 3667 See also: 3668 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3669 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3670 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3671 - `OverviewUserInfo()` method 3672 3673 :return: dict with raw data from server that contains user's information. Example of dict: 3674 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3675 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3676 """ 3677 uLogger.debug("Requesting common user's information. Wait, please...") 3678 3679 self.body = str({}) 3680 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3681 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3682 3683 uLogger.debug("Records about current user successfully received") 3684 3685 return rawUserInfo 3686 3687 def RequestMarginStatus(self, accountId: str = None) -> dict: 3688 """ 3689 Method for requesting margin calculation for defined account ID. 3690 3691 See also: 3692 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3693 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3694 - `OverviewUserInfo()` method 3695 3696 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3697 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3698 Example of responses: 3699 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3700 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3701 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3702 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3703 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3704 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3705 """ 3706 if accountId is None or not accountId: 3707 if self.accountId is None or not self.accountId: 3708 uLogger.error("Variable `accountId` must be defined for using this method!") 3709 raise Exception("Account ID required") 3710 3711 else: 3712 accountId = self.accountId # use `self.accountId` (main ID) by default 3713 3714 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3715 3716 self.body = str({"accountId": accountId}) 3717 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3718 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3719 3720 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3721 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3722 rawMargin = {} 3723 3724 else: 3725 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3726 3727 return rawMargin 3728 3729 def RequestTariffLimits(self) -> dict: 3730 """ 3731 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3732 3733 See also: 3734 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3735 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3736 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3737 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3738 - `OverviewUserInfo()` method 3739 3740 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3741 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3742 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3743 """ 3744 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3745 3746 self.body = str({}) 3747 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3748 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3749 3750 uLogger.debug("Records with limits of current tariff successfully received") 3751 3752 return rawTariffLimits 3753 3754 def RequestBondCoupons(self, iJSON: dict) -> dict: 3755 """ 3756 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3757 then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". 3758 All dates are in UTC timezone. 3759 3760 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3761 Documentation: 3762 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3763 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3764 3765 See also: `ExtendBondsData()`. 3766 3767 :param iJSON: raw json data of a bond from broker server, example: `iJSON = self.iList["Bonds"][self.ticker]` 3768 If raw iJSON is not data of bond then server returns an error [400] with message: 3769 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3770 :return: dictionary with bond payment calendar. Response example: 3771 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3772 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3773 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3774 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3775 """ 3776 if iJSON["figi"] is None or not iJSON["figi"]: 3777 uLogger.error("FIGI must be defined for using this method!") 3778 raise Exception("FIGI required") 3779 3780 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3781 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3782 3783 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3784 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3785 self.figi, 3786 startDate, 3787 endDate, 3788 )) 3789 3790 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3791 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3792 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3793 3794 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3795 uLogger.warning("Instrument type is not bond!") 3796 3797 else: 3798 uLogger.debug("Records about bond payment calendar successfully received") 3799 3800 return calendar 3801 3802 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3803 """ 3804 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3805 pandas dataframe with more information about bonds: main info, current prices, bond payment calendar, 3806 coupon yields, current yields and some statistics etc. 3807 3808 WARNING! This is too long operation if a lot of bonds requested from broker server. 3809 3810 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3811 3812 :param instruments: list of strings with tickers or FIGIs. 3813 :param xlsx: if True then also exports pandas dataframe to xlsx-file `bondsXLSXFile`, default: `ext-bonds.xlsx`, 3814 for further used by data scientists or stock analytics. 3815 :return: wider pandas dataframe with more full and calculated data about bonds, than raw response from broker. 3816 In XLSX-file and pandas dataframe fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3817 """ 3818 if instruments is None or not instruments: 3819 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3820 raise Exception("Ticker or FIGI required") 3821 3822 if isinstance(instruments, str): 3823 instruments = [instruments] 3824 3825 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3826 3827 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3828 3829 iCount = len(uniqueInstruments) 3830 tooLong = iCount >= 20 3831 if tooLong: 3832 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3833 3834 bonds = None 3835 for i, self.figi in enumerate(uniqueInstruments): 3836 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3837 3838 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3839 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3840 rawBond = self.SearchByFIGI(requestPrice=True) 3841 3842 # Widen raw data with UTC current time (iData["actualDateTime"]): 3843 actualDate = datetime.now(tzutc()) 3844 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3845 3846 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3847 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3848 3849 # Replace some values with human-readable: 3850 iData["nominalCurrency"] = iData["nominal"]["currency"] 3851 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3852 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3853 iData["aciCurrency"] = iData["aciValue"]["currency"] 3854 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3855 iData["issueSize"] = int(iData["issueSize"]) 3856 iData["issueSizePlan"] = int(iData["issueSize"]) 3857 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3858 iData["minPriceIncrement"] = NanoToFloat(iData["minPriceIncrement"]["units"], iData["minPriceIncrement"]["nano"]) if "minPriceIncrement" in iData.keys() else 0. 3859 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3860 3861 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3862 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3863 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3864 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3865 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3866 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3867 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3868 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3869 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3870 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3871 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3872 3873 # Widen raw data with calendar data from `rawCalendar` values: 3874 calendarData = [] 3875 for item in iData["rawCalendar"]["events"]: 3876 calendarData.append({ 3877 "couponDate": item["couponDate"], 3878 "couponNumber": int(item["couponNumber"]), 3879 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3880 "payCurrency": item["payOneBond"]["currency"], 3881 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3882 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3883 "couponStartDate": item["couponStartDate"], 3884 "couponEndDate": item["couponEndDate"], 3885 "couponPeriod": item["couponPeriod"], 3886 }) 3887 3888 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3889 if "maturityDate" not in iData.keys(): 3890 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3891 3892 # Widen raw data with Coupon Rate. 3893 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3894 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3895 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3896 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3897 3898 # Widen raw data with Yield to Maturity (YTM) on current date. 3899 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3900 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3901 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3902 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3903 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3904 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3905 3906 iData["calendar"] = calendarData # adds calendar at the end 3907 3908 # Remove not used data: 3909 iData.pop("uid") 3910 iData.pop("positionUid") 3911 iData.pop("currentPrice") 3912 iData.pop("rawCalendar") 3913 3914 colNames = list(iData.keys()) 3915 if bonds is None: 3916 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3917 3918 else: 3919 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3920 3921 else: 3922 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3923 3924 processed = round(100 * (i + 1) / iCount, 1) 3925 if tooLong and processed % 5 == 0: 3926 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3927 3928 else: 3929 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3930 3931 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3932 3933 # Saving bonds from pandas dataframe to XLSX sheet: 3934 if xlsx and self.bondsXLSXFile: 3935 with pd.ExcelWriter( 3936 path=self.bondsXLSXFile, 3937 date_format=TKS_DATE_FORMAT, 3938 datetime_format=TKS_DATE_TIME_FORMAT, 3939 mode="w", 3940 ) as writer: 3941 bonds.to_excel( 3942 writer, 3943 sheet_name="Extended bonds data", 3944 index=True, 3945 encoding="UTF-8", 3946 freeze_panes=(1, 1), 3947 ) # saving as XLSX-file with freeze first row and column as headers 3948 3949 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3950 3951 return bonds 3952 3953 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 3954 """ 3955 Creates bond payments calendar as pandas dataframe, and you can also save it to the XLSX-file. 3956 3957 WARNING! This is too long operation if a lot of bonds requested from broker server. 3958 3959 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 3960 3961 :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains 3962 extended information about bonds: main info, current prices, bond payment calendar, 3963 coupon yields, current yields and some statistics etc. 3964 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 3965 :param xlsx: if True then also exports pandas dataframe to file `calendarFile` + `".xlsx"`, default: `calendar.xlsx`, 3966 for further used by data scientists or stock analytics. 3967 :return: pandas dataframe with only bond payments calendar data. 3968 """ 3969 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 3970 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 3971 3972 uLogger.debug("Generating bond payments calendar data. Wait, please...") 3973 3974 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 3975 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 3976 calendar = None 3977 for bond in extBonds.iterrows(): 3978 for item in bond[1]["calendar"]: 3979 cData = { 3980 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 3981 "couponDate": item["couponDate"], 3982 "figi": bond[1]["figi"], 3983 "ticker": bond[1]["ticker"], 3984 "name": bond[1]["name"], 3985 "couponNumber": item["couponNumber"], 3986 "payOneBond": item["payOneBond"], 3987 "payCurrency": item["payCurrency"], 3988 "couponType": item["couponType"], 3989 "couponPeriod": item["couponPeriod"], 3990 "fixDate": item["fixDate"], 3991 "couponStartDate": item["couponStartDate"], 3992 "couponEndDate": item["couponEndDate"], 3993 } 3994 3995 if calendar is None: 3996 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 3997 3998 else: 3999 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4000 4001 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4002 4003 # Saving calendar from pandas dataframe to XLSX sheet: 4004 if xlsx: 4005 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4006 4007 with pd.ExcelWriter( 4008 path=xlsxCalendarFile, 4009 date_format=TKS_DATE_FORMAT, 4010 datetime_format=TKS_DATE_TIME_FORMAT, 4011 mode="w", 4012 ) as writer: 4013 humanReadable = calendar.copy(deep=True) 4014 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4015 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4016 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4017 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4018 humanReadable.columns = colNames # human-readable column names 4019 4020 humanReadable.to_excel( 4021 writer, 4022 sheet_name="Bond payments calendar", 4023 index=False, 4024 encoding="UTF-8", 4025 freeze_panes=(1, 2), 4026 ) # saving as XLSX-file with freeze first row and column as headers 4027 4028 del humanReadable # release df in memory 4029 4030 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4031 4032 return calendar 4033 4034 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4035 """ 4036 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4037 4038 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 4039 4040 :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains 4041 extended information about bonds: main info, current prices, bond payment calendar, 4042 coupon yields, current yields and some statistics etc. 4043 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4044 :param show: if `True` then also printing bonds payment calendar to the console, 4045 otherwise save to file `calendarFile` only. `False` by default. 4046 :return: multilines text in Markdown format with bonds payment calendar as a table. 4047 """ 4048 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4049 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4050 4051 infoText = "# Bond payments calendar\n\n" 4052 4053 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate pandas dataframe with full calendar data 4054 4055 if not calendar.empty: 4056 splitLine = "| | | | | | | | | |\n" 4057 4058 info = [ 4059 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4060 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4061 ] 4062 4063 newMonth = False 4064 notOneBond = calendar["figi"].nunique() > 1 4065 for i, bond in enumerate(calendar.iterrows()): 4066 if newMonth and notOneBond: 4067 info.append(splitLine) 4068 4069 info.append( 4070 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4071 " +" if bond[1]["paid"] else " —", 4072 bond[1]["couponDate"].split("T")[0], 4073 bond[1]["figi"], 4074 bond[1]["ticker"], 4075 bond[1]["couponNumber"], 4076 "{} {}".format( 4077 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4078 bond[1]["payCurrency"], 4079 ), 4080 bond[1]["couponType"], 4081 bond[1]["couponPeriod"], 4082 bond[1]["fixDate"].split("T")[0], 4083 ) 4084 ) 4085 4086 if i < len(calendar.values) - 1: 4087 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4088 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4089 newMonth = False if curDate.month == nextDate.month else True 4090 4091 else: 4092 newMonth = False 4093 4094 infoText += "".join(info) 4095 4096 if show: 4097 uLogger.info("{}".format(infoText)) 4098 4099 if self.calendarFile is not None: 4100 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4101 fH.write(infoText) 4102 4103 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4104 4105 else: 4106 infoText += "No data\n" 4107 4108 return infoText 4109 4110 def OverviewAccounts(self, show: bool = False) -> dict: 4111 """ 4112 Method for parsing and show simple table with all available user accounts. 4113 4114 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4115 4116 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4117 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4118 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4119 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4120 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4121 "closed": "—", "access": "Full access" }, ...}}` 4122 """ 4123 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4124 4125 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4126 accounts = { 4127 item["id"]: { 4128 "type": TKS_ACCOUNT_TYPES[item["type"]], 4129 "name": item["name"], 4130 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4131 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4132 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4133 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4134 } for item in rawAccounts["accounts"] 4135 } 4136 4137 # Raw and parsed data with some fields replaced in "stat" section: 4138 view = { 4139 "rawAccounts": rawAccounts, 4140 "stat": accounts, 4141 } 4142 4143 # --- Prepare simple text table with only accounts data in human-readable format: 4144 if show: 4145 info = [ 4146 "# User accounts\n\n", 4147 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4148 "| Account ID | Type | Status | Name |\n", 4149 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4150 ] 4151 4152 for account in view["stat"].keys(): 4153 info.extend([ 4154 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4155 account, 4156 view["stat"][account]["type"], 4157 view["stat"][account]["status"], 4158 view["stat"][account]["name"], 4159 ) 4160 ]) 4161 4162 infoText = "".join(info) 4163 4164 uLogger.info(infoText) 4165 4166 if self.userAccountsFile: 4167 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4168 fH.write(infoText) 4169 4170 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4171 4172 return view 4173 4174 def OverviewUserInfo(self, show: bool = False) -> dict: 4175 """ 4176 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4177 4178 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4179 4180 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4181 :return: dict with raw parsed data from server and some calculated statistics about it. 4182 """ 4183 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4184 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4185 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4186 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4187 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4188 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4189 4190 # This is dict with parsed common user data: 4191 userInfo = { 4192 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4193 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4194 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4195 "tariff": rawUserInfo["tariff"], 4196 } 4197 4198 # This is an array of dict with parsed margin statuses for every account IDs: 4199 margins = {} 4200 for accountId in accounts.keys(): 4201 if rawMargins[accountId]: 4202 margins[accountId] = { 4203 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4204 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4205 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4206 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4207 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4208 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4209 } 4210 4211 else: 4212 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4213 4214 unary = {} # unary-connection limits 4215 for item in rawTariffLimits["unaryLimits"]: 4216 if item["limitPerMinute"] in unary.keys(): 4217 unary[item["limitPerMinute"]].extend(item["methods"]) 4218 4219 else: 4220 unary[item["limitPerMinute"]] = item["methods"] 4221 4222 stream = {} # stream-connection limits 4223 for item in rawTariffLimits["streamLimits"]: 4224 if item["limit"] in stream.keys(): 4225 stream[item["limit"]].extend(item["streams"]) 4226 4227 else: 4228 stream[item["limit"]] = item["streams"] 4229 4230 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4231 limits = { 4232 "unary": unary, 4233 "stream": stream, 4234 } 4235 4236 # Raw and parsed data as an output result: 4237 view = { 4238 "rawUserInfo": rawUserInfo, 4239 "rawAccounts": rawAccounts, 4240 "rawMargins": rawMargins, 4241 "rawTariffLimits": rawTariffLimits, 4242 "stat": { 4243 "userInfo": userInfo, 4244 "accounts": accounts, 4245 "margins": margins, 4246 "limits": limits, 4247 }, 4248 } 4249 4250 # --- Prepare text table with user information in human-readable format: 4251 if show: 4252 info = [ 4253 "# Full user information\n\n", 4254 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4255 "## Common information\n\n", 4256 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4257 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4258 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4259 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4260 "\n## User accounts\n\n", 4261 ] 4262 4263 for account in view["stat"]["accounts"].keys(): 4264 info.extend([ 4265 "### ID: [{}]\n\n".format(account), 4266 "| Parameters | Values |\n", 4267 "|----------------------|--------------------------------------------------------------|\n", 4268 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4269 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4270 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4271 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4272 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4273 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4274 ]) 4275 4276 if margins[account]: 4277 info.extend([ 4278 "| Margin status: | Enabled |\n", 4279 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4280 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4281 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4282 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4283 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4284 ]) 4285 4286 else: 4287 info.append("| Margin status: | Disabled |\n\n") 4288 4289 info.extend([ 4290 "\n## Current user tariff limits\n", 4291 "\nSee also:\n", 4292 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4293 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4294 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4295 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4296 "\n### Unary limits\n", 4297 ]) 4298 4299 if unary: 4300 for key, values in sorted(unary.items()): 4301 info.append("\n* Max requests per minute: {}\n".format(key)) 4302 4303 for value in values: 4304 info.append(" - {}\n".format(value)) 4305 4306 else: 4307 info.append("\nNot available\n") 4308 4309 info.append("\n### Stream limits\n") 4310 4311 if stream: 4312 for key, values in sorted(stream.items()): 4313 info.append("\n* Max stream connections: {}\n".format(key)) 4314 4315 for value in values: 4316 info.append(" - {}\n".format(value)) 4317 4318 else: 4319 info.append("\nNot available\n") 4320 4321 infoText = "".join(info) 4322 4323 uLogger.info(infoText) 4324 4325 if self.userInfoFile: 4326 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4327 fH.write(infoText) 4328 4329 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4330 4331 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
196 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 197 """ 198 Main class init. 199 200 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 201 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 202 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 203 :param useCache: use default cache file with raw data to use instead of `iList`. 204 True by default. Cache is auto-update if new day has come. 205 If you don't want to use cache and always updates raw data then set `useCache=False`. 206 :param defaultCache: path to default cache file. `dump.json` by default. 207 """ 208 if token is None or not token: 209 try: 210 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 211 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 212 213 except KeyError: 214 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 215 raise Exception("Token required") 216 217 else: 218 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 219 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 220 221 if accountId is None or not accountId: 222 try: 223 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 224 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 225 226 except KeyError: 227 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 228 229 else: 230 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 231 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 232 233 self.version = __version__ # duplicate here used TKSBrokerAPI main version 234 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 235 236 Latest version: https://pypi.org/project/tksbrokerapi/ 237 """ 238 239 self.aliases = TKS_TICKER_ALIASES 240 """Some aliases instead official tickers. 241 242 See also: `TKSEnums.TKS_TICKER_ALIASES` 243 """ 244 245 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 246 247 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 248 249 self.ticker = "" 250 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 251 252 See also: `SearchByTicker()`, `SearchInstruments()`. 253 """ 254 255 self.figi = "" 256 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 257 258 See also: `SearchByFIGI()`, `SearchInstruments()`. 259 """ 260 261 self.depth = 1 262 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 263 264 See also: `GetCurrentPrices()`. 265 """ 266 267 self.server = r"https://invest-public-api.tinkoff.ru/rest" 268 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 269 270 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 271 """ 272 273 uLogger.debug("Broker API server: {}".format(self.server)) 274 275 self.timeout = 15 276 """Server operations timeout in seconds. Default: `15`. 277 278 See also: `SendAPIRequest()`. 279 """ 280 281 self.headers = {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {}".format(self.token)} 282 """Headers which send in every request to broker server. Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 283 284 See also: `SendAPIRequest()`. 285 """ 286 287 self.body = None 288 """Request body which send to broker server. Default: `None`. 289 290 See also: `SendAPIRequest()`. 291 """ 292 293 self.historyFile = None 294 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only pandas dataframe. 295 296 See also: `History()`. 297 """ 298 299 self.htmlHistoryFile = "index.html" 300 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 301 302 See also: `ShowHistoryChart()`. 303 """ 304 305 self.instrumentsFile = "instruments.md" 306 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 307 308 See also: `ShowInstrumentsInfo()`. 309 """ 310 311 self.searchResultsFile = "search-results.md" 312 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 313 314 See also: `SearchInstruments()`. 315 """ 316 317 self.pricesFile = "prices.md" 318 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 319 320 See also: `GetListOfPrices()`. 321 """ 322 323 self.infoFile = "info.md" 324 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 325 326 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 327 """ 328 329 self.bondsXLSXFile = "ext-bonds.xlsx" 330 """Filename where wider pandas dataframe with more information about bonds: main info, current prices, 331 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 332 333 See also: `ExtendBondsData()`. 334 """ 335 336 self.calendarFile = "calendar.md" 337 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 338 339 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 340 341 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 342 """ 343 344 self.overviewFile = "overview.md" 345 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 346 347 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 348 """ 349 350 self.overviewDigestFile = "overview-digest.md" 351 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 352 353 See also: `Overview()` with parameter `details="digest"`. 354 """ 355 356 self.overviewPositionsFile = "overview-positions.md" 357 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 358 359 See also: `Overview()` with parameter `details="positions"`. 360 """ 361 362 self.overviewOrdersFile = "overview-orders.md" 363 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 364 365 See also: `Overview()` with parameter `details="orders"`. 366 """ 367 368 self.overviewAnalyticsFile = "overview-analytics.md" 369 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 370 371 See also: `Overview()` with parameter `details="analytics"`. 372 """ 373 374 self.reportFile = "deals.md" 375 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 376 377 See also: `Deals()`. 378 """ 379 380 self.withdrawalLimitsFile = "limits.md" 381 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 382 383 See also: `OverviewLimits()` and `RequestLimits()`. 384 """ 385 386 self.userInfoFile = "user-info.md" 387 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 388 389 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 390 """ 391 392 self.userAccountsFile = "accounts.md" 393 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 394 395 See also: `OverviewAccounts()`, `RequestAccounts()`. 396 """ 397 398 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 399 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 400 401 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 402 403 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 404 """ 405 406 self.iList = None # init iList for raw instruments data 407 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 408 409 See also: `Listing()`, `DumpInstruments()`. 410 """ 411 412 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 413 if useCache: 414 if os.path.exists(self.iListDumpFile): 415 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 416 curTime = datetime.now(tzutc()) 417 418 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 419 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 420 421 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 422 423 else: 424 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 425 426 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 427 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 428 429 else: 430 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 431 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 432 433 else: 434 self.iList = self.Listing() # request new raw instruments data from broker server 435 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 436 437 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 438 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 439 440 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 441 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
String with ticker, e.g. GOOGL. Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6.
See also: SearchByFIGI(), SearchInstruments().
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only pandas dataframe.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider pandas dataframe with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
465 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 466 """ 467 Send GET or POST request to broker server and receive JSON object. 468 469 self.header: must be defining with dictionary of headers. 470 self.body: if define then used as request body. None by default. 471 self.timeout: global request timeout, 15 seconds by default. 472 :param url: url with REST request. 473 :param reqType: send "GET" or "POST" request. "GET" by default. 474 :param retry: how many times retry after first request if an 5xx server errors occurred. 475 :param pause: sleep time in seconds between retries. 476 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 477 :return: response JSON (dictionary) from broker. 478 """ 479 if reqType not in ("GET", "POST"): 480 uLogger.error("You can define request type: 'GET' or 'POST'!") 481 raise Exception("Incorrect value") 482 483 if debug: 484 uLogger.debug("Request parameters:") 485 uLogger.debug(" - REST API URL: {}".format(url)) 486 uLogger.debug(" - request type: {}".format(reqType)) 487 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 488 uLogger.debug(" - body: {}".format(self.body)) 489 490 # fast hack to avoid all operations with some tickers/FIGI 491 responseJSON = {} 492 oK = True 493 for item in self.exclude: 494 if item in url: 495 if debug: 496 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 497 498 oK = False 499 break 500 501 if oK: 502 counter = 0 503 response = None 504 errMsg = "" 505 506 while not response and counter <= retry: 507 if reqType == "GET": 508 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 509 510 if reqType == "POST": 511 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 512 513 if debug: 514 uLogger.debug("Response:") 515 uLogger.debug(" - status code: {}".format(response.status_code)) 516 uLogger.debug(" - reason: {}".format(response.reason)) 517 uLogger.debug(" - body length: {}".format(len(response.text))) 518 uLogger.debug(" - headers: {}".format(response.headers)) 519 520 # Server returns some headers: 521 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 522 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 523 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 524 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 525 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 526 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 527 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 528 sleep(rateLimitWait) 529 530 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 531 if 400 <= response.status_code < 500: 532 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 533 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 534 counter = retry + 1 535 536 if 500 <= response.status_code < 600: 537 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 538 uLogger.debug(" - not oK, {}".format(errMsg)) 539 counter += 1 540 541 if counter <= retry: 542 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 543 sleep(pause) 544 545 responseJSON = self._ParseJSON(response.text) 546 547 if errMsg: 548 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 549 uLogger.error(" - not oK, {}".format(errMsg)) 550 551 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
- debug: if
Truethen print more debug information, e.g. request and response parameters, headers etc.
Returns
response JSON (dictionary) from broker.
584 def Listing(self) -> dict: 585 """ 586 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 587 588 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 589 """ 590 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 591 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 592 593 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 594 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 595 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 596 597 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 598 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 599 poolUpdater.close() 600 601 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 602 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 603 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 604 605 # calculate minimum price increment (step) for all instruments and set up instrument's type: 606 for iType in iList.keys(): 607 for ticker in iList[iType]: 608 iList[iType][ticker]["type"] = iType 609 610 if "minPriceIncrement" in iList[iType][ticker].keys(): 611 iList[iType][ticker]["step"] = NanoToFloat( 612 iList[iType][ticker]["minPriceIncrement"]["units"], 613 iList[iType][ticker]["minPriceIncrement"]["nano"], 614 ) 615 616 else: 617 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 618 619 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
621 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 622 """ 623 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 624 625 See also: `DumpInstruments()`, `Listing()`. 626 627 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 628 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 629 """ 630 if self.iListDumpFile is None or not self.iListDumpFile: 631 uLogger.error("Output name of dump file must be defined!") 632 raise Exception("Filename required") 633 634 if not self.iList or forceUpdate: 635 self.iList = self.Listing() 636 637 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 638 639 # Save as XLSX with separated sheets for every type of instruments: 640 with pd.ExcelWriter( 641 path=xlsxDumpFile, 642 date_format=TKS_DATE_FORMAT, 643 datetime_format=TKS_DATE_TIME_FORMAT, 644 mode="w", 645 ) as writer: 646 for iType in TKS_INSTRUMENTS: 647 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 648 df = df[sorted(df)] # sorted by column names 649 df = df.applymap( 650 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 651 na_action="ignore", 652 ) # converting numbers from nano-type to float in every cell 653 df.to_excel( 654 writer, 655 sheet_name=iType, 656 encoding="UTF-8", 657 freeze_panes=(1, 1), 658 ) # saving as XLSX-file with freeze first row and column as headers 659 660 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
662 def DumpInstruments(self, forceUpdate: bool = True) -> str: 663 """ 664 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 665 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 666 667 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 668 669 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 670 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 671 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 672 """ 673 if self.iListDumpFile is None or not self.iListDumpFile: 674 uLogger.error("Output name of dump file must be defined!") 675 raise Exception("Filename required") 676 677 if not self.iList or forceUpdate: 678 self.iList = self.Listing() 679 680 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 681 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 682 fH.write(jsonDump) 683 684 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 685 686 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
688 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 689 """ 690 Show information about one instrument defined by json data and prints it in Markdown format. 691 692 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 693 694 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 695 :param show: if `True` then also printing information about instrument and its current price. 696 :return: multilines text in Markdown format with information about one instrument. 697 """ 698 splitLine = "| | |\n" 699 infoText = "" 700 701 if iJSON is not None and iJSON and isinstance(iJSON, dict): 702 info = [ 703 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 704 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 705 "| Parameters | Values |\n", 706 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 707 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 708 "| Full name: | {:<54} |\n".format(iJSON["name"]), 709 ] 710 711 if "sector" in iJSON.keys() and iJSON["sector"]: 712 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 713 714 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 715 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 716 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 717 ))) 718 719 info.extend([ 720 splitLine, 721 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 722 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 723 ]) 724 725 if "isin" in iJSON.keys() and iJSON["isin"]: 726 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 727 728 if "classCode" in iJSON.keys(): 729 info.append("| Class Code: | {:<54} |\n".format(iJSON["classCode"])) 730 731 info.extend([ 732 splitLine, 733 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 734 splitLine, 735 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 736 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 737 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 738 ]) 739 740 if iJSON["figi"]: 741 self.figi = iJSON["figi"] 742 iJSON = iJSON | self.RequestTradingStatus() 743 744 info.extend([ 745 splitLine, 746 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 747 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 748 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 749 ]) 750 751 info.append(splitLine) 752 753 if "type" in iJSON.keys() and iJSON["type"]: 754 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 755 756 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 757 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 758 759 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 760 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 761 762 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 763 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 764 765 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 766 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 767 768 if "focusType" in iJSON.keys() and iJSON["focusType"]: 769 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 770 771 if "assetType" in iJSON.keys() and iJSON["assetType"]: 772 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 773 774 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 775 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 776 777 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 778 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 779 780 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 781 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 782 783 if "currency" in iJSON.keys(): 784 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 785 786 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 787 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 788 789 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 790 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 791 792 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 793 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 794 795 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 796 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 797 798 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 799 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 800 801 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 802 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 803 804 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 805 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 806 807 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 808 info.append("| Perpetual bond: | Yes |\n") 809 810 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 811 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 812 813 iExt = None 814 if iJSON["type"] == "Bonds": 815 info.extend([ 816 splitLine, 817 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 818 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 819 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 820 iJSON["nominal"]["currency"], 821 )), 822 ]) 823 824 if "floatingCouponFlag" in iJSON.keys(): 825 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 826 827 if "amortizationFlag" in iJSON.keys(): 828 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 829 830 info.append(splitLine) 831 832 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 833 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 834 835 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 836 837 info.extend([ 838 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 839 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 840 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 841 ]) 842 843 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 844 info.append("| Current Accrued Interest (ACI): | {:<54} |\n".format("{:.2f} {}".format( 845 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 846 iJSON["aciValue"]["currency"] 847 ))) 848 849 if "currentPrice" in iJSON.keys(): 850 info.append(splitLine) 851 852 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 853 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 854 855 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 856 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 857 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 858 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 859 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 860 861 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 862 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 863 864 info.extend([ 865 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 866 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 867 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 868 )), 869 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 870 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 871 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 872 )), 873 "| Changes between last deal price and last close | {:<54} |\n".format( 874 "{:.2f}%{}".format( 875 iJSON["currentPrice"]["changes"], 876 " ({}{:.2f} {})".format( 877 "+" if bondChangesDelta > 0 else "", 878 bondChangesDelta, 879 aciCurrency 880 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 881 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 882 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 883 currency 884 ), 885 ) 886 ), 887 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 888 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 889 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 890 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 891 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 892 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 893 )), 894 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 895 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 896 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 897 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 898 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 899 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 900 )), 901 ]) 902 903 if "lot" in iJSON.keys(): 904 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 905 906 if "step" in iJSON.keys() and iJSON["step"] != 0: 907 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 908 909 # Add bond payment calendar: 910 if iJSON["type"] == "Bonds": 911 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 912 info.extend(["\n", strCalendar]) 913 914 infoText += "".join(info) 915 916 if show: 917 uLogger.info("{}".format(infoText)) 918 919 else: 920 uLogger.debug("{}".format(infoText)) 921 922 if self.infoFile is not None: 923 with open(self.infoFile, "w", encoding="UTF-8") as fH: 924 fH.write(infoText) 925 926 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 927 928 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self.ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
930 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 931 """ 932 Search and return raw broker's information about instrument by its ticker. 933 `ticker` must be defined! If debug=True then print all debug messages. 934 935 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 936 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 937 :param debug: if `True` then print all debug console messages. 938 :return: JSON formatted data with information about instrument. 939 """ 940 tickerJSON = {} 941 if debug: 942 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 943 944 if not self.ticker: 945 uLogger.warning("self.ticker variable is not be empty!") 946 947 else: 948 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 949 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 950 raise Exception("Instrument not allowed") 951 952 if not self.iList: 953 self.iList = self.Listing() 954 955 if self.ticker in self.iList["Shares"].keys(): 956 tickerJSON = self.iList["Shares"][self.ticker] 957 if debug: 958 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 959 960 elif self.ticker in self.iList["Currencies"].keys(): 961 tickerJSON = self.iList["Currencies"][self.ticker] 962 if debug: 963 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 964 965 elif self.ticker in self.iList["Bonds"].keys(): 966 tickerJSON = self.iList["Bonds"][self.ticker] 967 if debug: 968 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 969 970 elif self.ticker in self.iList["Etfs"].keys(): 971 tickerJSON = self.iList["Etfs"][self.ticker] 972 if debug: 973 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 974 975 elif self.ticker in self.iList["Futures"].keys(): 976 tickerJSON = self.iList["Futures"][self.ticker] 977 if debug: 978 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 979 980 if tickerJSON: 981 self.figi = tickerJSON["figi"] 982 983 if requestPrice: 984 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 985 986 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 987 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 988 989 else: 990 tickerJSON["currentPrice"]["changes"] = 0 991 992 if show: 993 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 994 995 else: 996 if show: 997 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 998 999 return tickerJSON
Search and return raw broker's information about instrument by its ticker.
ticker must be defined! If debug=True then print all debug messages.
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console. - debug: if
Truethen print all debug console messages.
Returns
JSON formatted data with information about instrument.
1001 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1002 """ 1003 Search and return raw broker's information about instrument by its FIGI. 1004 `figi` must be defined! If debug=True then print all debug messages. 1005 1006 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1007 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1008 :param debug: if `True` then print all debug console messages. 1009 :return: JSON formatted data with information about instrument. 1010 """ 1011 figiJSON = {} 1012 if debug: 1013 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1014 1015 if not self.figi: 1016 uLogger.warning("self.figi variable is not be empty!") 1017 1018 else: 1019 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1020 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1021 raise Exception("Instrument not allowed") 1022 1023 if not self.iList: 1024 self.iList = self.Listing() 1025 1026 for item in self.iList["Shares"].keys(): 1027 if self.figi == self.iList["Shares"][item]["figi"]: 1028 figiJSON = self.iList["Shares"][item] 1029 1030 if debug: 1031 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1032 1033 break 1034 1035 if not figiJSON: 1036 for item in self.iList["Currencies"].keys(): 1037 if self.figi == self.iList["Currencies"][item]["figi"]: 1038 figiJSON = self.iList["Currencies"][item] 1039 1040 if debug: 1041 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1042 1043 break 1044 1045 if not figiJSON: 1046 for item in self.iList["Bonds"].keys(): 1047 if self.figi == self.iList["Bonds"][item]["figi"]: 1048 figiJSON = self.iList["Bonds"][item] 1049 1050 if debug: 1051 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1052 1053 break 1054 1055 if not figiJSON: 1056 for item in self.iList["Etfs"].keys(): 1057 if self.figi == self.iList["Etfs"][item]["figi"]: 1058 figiJSON = self.iList["Etfs"][item] 1059 1060 if debug: 1061 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1062 1063 break 1064 1065 if not figiJSON: 1066 for item in self.iList["Futures"].keys(): 1067 if self.figi == self.iList["Futures"][item]["figi"]: 1068 figiJSON = self.iList["Futures"][item] 1069 1070 if debug: 1071 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1072 1073 break 1074 1075 if figiJSON: 1076 self.figi = figiJSON["figi"] 1077 self.ticker = figiJSON["ticker"] 1078 1079 if requestPrice: 1080 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1081 1082 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1083 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1084 1085 else: 1086 figiJSON["currentPrice"]["changes"] = 0 1087 1088 if show: 1089 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1090 1091 else: 1092 if show: 1093 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1094 1095 return figiJSON
Search and return raw broker's information about instrument by its FIGI.
figi must be defined! If debug=True then print all debug messages.
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console. - debug: if
Truethen print all debug console messages.
Returns
JSON formatted data with information about instrument.
1097 def GetCurrentPrices(self, show: bool = True) -> dict: 1098 """ 1099 Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record: 1100 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1101 1102 See also: 1103 1104 :param show: if `True` then print DOM to log and console. 1105 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1106 """ 1107 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1108 1109 if self.depth < 1: 1110 uLogger.error("Depth of Market (DOM) must be >=1!") 1111 raise Exception("Incorrect value") 1112 1113 if not (self.ticker or self.figi): 1114 uLogger.error("self.ticker or self.figi variables must be defined!") 1115 raise Exception("Ticker or FIGI required") 1116 1117 if self.ticker and not self.figi: 1118 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1119 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1120 1121 if not self.ticker and self.figi: 1122 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1123 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1124 1125 if not self.figi: 1126 uLogger.error("FIGI is not defined!") 1127 raise Exception("Ticker or FIGI required") 1128 1129 else: 1130 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1131 1132 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1133 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1134 self.body = str({"figi": self.figi, "depth": self.depth}) 1135 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") 1136 1137 if pricesResponse: 1138 # list of dicts with sellers orders: 1139 prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1140 1141 # list of dicts with buyers orders: 1142 prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1143 1144 # max price of instrument at this time: 1145 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1146 1147 # min price of instrument at this time: 1148 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1149 1150 # last price of deal with instrument: 1151 prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0 1152 1153 # last close price of instrument: 1154 prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0 1155 1156 else: 1157 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1158 uLogger.debug("Server response: {}".format(pricesResponse)) 1159 1160 if show: 1161 if prices["buy"] or prices["sell"]: 1162 info = [ 1163 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1164 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1165 self.ticker, 1166 self.figi, 1167 self.depth, 1168 ), 1169 uLog.sepShort, "\n", 1170 " Orders of Buyers | Orders of Sellers\n", 1171 uLog.sepShort, "\n", 1172 " Sell prices (vol.) | Buy prices (vol.)\n", 1173 uLog.sepShort, "\n", 1174 ] 1175 1176 if not prices["buy"]: 1177 info.append(" | No orders!\n") 1178 sumBuy = 0 1179 1180 else: 1181 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1182 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1183 for item in maxMinSorted: 1184 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1185 1186 if not prices["sell"]: 1187 info.append("No orders! |\n") 1188 sumSell = 0 1189 1190 else: 1191 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1192 for item in prices["sell"]: 1193 info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1194 1195 info.extend([ 1196 uLog.sepShort, "\n", 1197 "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1198 uLog.sepShort, "\n", 1199 ]) 1200 1201 infoText = "".join(info) 1202 1203 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1204 1205 else: 1206 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1207 1208 return prices
Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record:
{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
See also:
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}.
1210 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1211 """ 1212 This method get and show information about all available broker instruments for current user account. 1213 If `instrumentsFile` string is not empty then also save information to this file. 1214 1215 :param show: if `True` then print results to console, if `False` - print only to file. 1216 :return: multi-lines string with all available broker instruments 1217 """ 1218 if not self.iList: 1219 self.iList = self.Listing() 1220 1221 info = [ 1222 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1223 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1224 ] 1225 1226 # add instruments count by type: 1227 for iType in self.iList.keys(): 1228 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1229 1230 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1231 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1232 1233 # generating info tables with all instruments by type: 1234 for iType in self.iList.keys(): 1235 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1236 1237 for instrument in self.iList[iType].keys(): 1238 iName = self.iList[iType][instrument]["name"] # instrument's name 1239 if len(iName) > 57: 1240 iName = "{}...".format(iName[:54]) # right trim for a long string 1241 1242 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1243 self.iList[iType][instrument]["ticker"], 1244 iName, 1245 self.iList[iType][instrument]["figi"], 1246 self.iList[iType][instrument]["currency"], 1247 self.iList[iType][instrument]["lot"], 1248 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1249 )) 1250 1251 infoText = "".join(info) 1252 1253 if show: 1254 uLogger.info(infoText) 1255 1256 if self.instrumentsFile: 1257 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1258 fH.write(infoText) 1259 1260 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1261 1262 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse- print only to file.
Returns
multi-lines string with all available broker instruments
1264 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1265 """ 1266 This method search and show information about instruments by part of its ticker, FIGI or name. 1267 If `searchResultsFile` string is not empty then also save information to this file. 1268 1269 :param pattern: string with part of ticker, FIGI or instrument's name. 1270 :param show: if `True` then print results to console, if `False` - return list of result only. 1271 :return: list of dictionaries with all found instruments. 1272 """ 1273 if not self.iList: 1274 self.iList = self.Listing() 1275 1276 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1277 compiledPattern = re.compile(pattern, re.IGNORECASE) 1278 1279 for iType in self.iList: 1280 for instrument in self.iList[iType].values(): 1281 searchResult = compiledPattern.search(" ".join( 1282 [instrument["ticker"], instrument["figi"], instrument["name"]] 1283 )) 1284 1285 if searchResult: 1286 searchResults[iType][instrument["ticker"]] = instrument 1287 1288 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1289 info = [ 1290 "# Search results\n\n", 1291 "* **Search pattern:** [{}]\n".format(pattern), 1292 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1293 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1294 ] 1295 infoShort = info[:] 1296 1297 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1298 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1299 skippedLine = "| ... | ... | ... | ... |\n" 1300 1301 if resultsLen == 0: 1302 info.append("\nNo results\n") 1303 infoShort.append("\nNo results\n") 1304 uLogger.warning("No results. Try changing your search pattern.") 1305 1306 else: 1307 for iType in searchResults: 1308 iTypeValuesCount = len(searchResults[iType].values()) 1309 if iTypeValuesCount > 0: 1310 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1311 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1312 1313 for instrument in searchResults[iType].values(): 1314 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1315 instrument["type"], 1316 instrument["ticker"], 1317 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1318 instrument["figi"], 1319 )) 1320 1321 if iTypeValuesCount <= 5: 1322 infoShort.extend(info[-iTypeValuesCount:]) 1323 1324 else: 1325 infoShort.extend(info[-5:]) 1326 infoShort.append(skippedLine) 1327 1328 infoText = "".join(info) 1329 infoTextShort = "".join(infoShort) 1330 1331 if show: 1332 uLogger.info(infoTextShort) 1333 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1334 1335 if self.searchResultsFile: 1336 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1337 fH.write(infoText) 1338 1339 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1340 1341 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse- return list of result only.
Returns
list of dictionaries with all found instruments.
1343 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1344 """ 1345 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1346 1347 :param instruments: list of strings with tickers or FIGIs. 1348 :return: list with unique instrument FIGIs only. 1349 """ 1350 requestedInstruments = [] 1351 for iName in instruments: 1352 if iName not in self.aliases.keys(): 1353 if iName not in requestedInstruments: 1354 requestedInstruments.append(iName) 1355 1356 else: 1357 if iName not in requestedInstruments: 1358 if self.aliases[iName] not in requestedInstruments: 1359 requestedInstruments.append(self.aliases[iName]) 1360 1361 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1362 1363 onlyUniqueFIGIs = [] 1364 for iName in requestedInstruments: 1365 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1366 continue 1367 1368 self.ticker = iName 1369 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1370 1371 if not iData: 1372 self.ticker = "" 1373 self.figi = iName 1374 1375 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1376 1377 if not iData: 1378 self.figi = "" 1379 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1380 1381 if iData and iData["figi"] not in onlyUniqueFIGIs: 1382 onlyUniqueFIGIs.append(iData["figi"]) 1383 1384 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1385 1386 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1388 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1389 """ 1390 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1391 See limits: https://tinkoff.github.io/investAPI/limits/ 1392 If `pricesFile` string is not empty then also save information to this file. 1393 1394 :param instruments: list of strings with tickers or FIGIs. 1395 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1396 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1397 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1398 """ 1399 if instruments is None or not instruments: 1400 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1401 raise Exception("Ticker or FIGI required") 1402 1403 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1404 1405 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1406 1407 iList = [] # trying to get info and current prices about all unique instruments: 1408 for self.figi in onlyUniqueFIGIs: 1409 iData = self.SearchByFIGI(requestPrice=True) 1410 iList.append(iData) 1411 1412 self.ShowListOfPrices(iList, show) 1413 1414 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse- prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1416 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1417 """ 1418 Show table contains current prices of given instruments. 1419 1420 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1421 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1422 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1423 :return: multilines text in Markdown format as a table contains current prices. 1424 """ 1425 infoText = "" 1426 1427 if show or self.pricesFile: 1428 info = [ 1429 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1430 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1431 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1432 ] 1433 1434 for item in iList: 1435 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1436 item["ticker"], 1437 item["figi"], 1438 item["type"], 1439 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1440 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1441 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1442 "{} / {}".format( 1443 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1444 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1445 ), 1446 "{} / {}".format( 1447 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1448 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1449 ), 1450 item["currency"], 1451 )) 1452 1453 infoText = "".join(info) 1454 1455 if show: 1456 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1457 1458 if self.pricesFile: 1459 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1460 fH.write(infoText) 1461 1462 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1463 1464 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse- prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1466 def RequestTradingStatus(self) -> dict: 1467 """ 1468 Requesting trading status for the instrument defined by `figi` variable. 1469 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1470 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1471 1472 :return: dictionary with trading status attributes. Response example: 1473 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1474 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1475 """ 1476 if self.figi is None or not self.figi: 1477 uLogger.error("Variable `figi` must be defined for using this method!") 1478 raise Exception("FIGI required") 1479 1480 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1481 1482 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1483 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1484 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1485 1486 uLogger.debug("Records about current trading status successfully received") 1487 1488 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1490 def RequestPortfolio(self) -> dict: 1491 """ 1492 Requesting actual user's portfolio for current `accountId`. 1493 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1494 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1495 1496 :return: dictionary with user's portfolio. 1497 """ 1498 if self.accountId is None or not self.accountId: 1499 uLogger.error("Variable `accountId` must be defined for using this method!") 1500 raise Exception("Account ID required") 1501 1502 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1503 1504 self.body = str({"accountId": self.accountId}) 1505 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1506 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1507 1508 uLogger.debug("Records about user's portfolio successfully received") 1509 1510 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1512 def RequestPositions(self) -> dict: 1513 """ 1514 Requesting open positions by currencies and instruments for current `accountId`. 1515 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1516 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1517 1518 :return: dictionary with open positions by instruments. 1519 """ 1520 if self.accountId is None or not self.accountId: 1521 uLogger.error("Variable `accountId` must be defined for using this method!") 1522 raise Exception("Account ID required") 1523 1524 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1525 1526 self.body = str({"accountId": self.accountId}) 1527 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1528 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1529 1530 uLogger.debug("Records about current open positions successfully received") 1531 1532 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1534 def RequestPendingOrders(self) -> list: 1535 """ 1536 Requesting current actual pending orders for current `accountId`. 1537 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1538 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1539 1540 :return: list of dictionaries with pending orders. 1541 """ 1542 if self.accountId is None or not self.accountId: 1543 uLogger.error("Variable `accountId` must be defined for using this method!") 1544 raise Exception("Account ID required") 1545 1546 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1547 1548 self.body = str({"accountId": self.accountId}) 1549 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1550 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1551 1552 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1553 1554 return rawOrders
Requesting current actual pending orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending orders.
1556 def RequestStopOrders(self) -> list: 1557 """ 1558 Requesting current actual stop orders for current `accountId`. 1559 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1560 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1561 1562 :return: list of dictionaries with stop orders. 1563 """ 1564 if self.accountId is None or not self.accountId: 1565 uLogger.error("Variable `accountId` must be defined for using this method!") 1566 raise Exception("Account ID required") 1567 1568 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1569 1570 self.body = str({"accountId": self.accountId}) 1571 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1572 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1573 1574 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1575 1576 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1578 def Overview(self, show: bool = False, details: str = "full") -> dict: 1579 """ 1580 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1581 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1582 are defined then also save information to file. 1583 1584 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1585 many requests about the state of the portfolio, and then, based on the received data, a large number 1586 of calculation and statistics are collected. 1587 1588 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1589 :param details: how detailed should the information be? You should specify one of strings: 1590 `full` - shows full available information about portfolio status (by default), 1591 `positions` - shows only open positions, 1592 `digest` - show a short digest of the portfolio status, 1593 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1594 `orders` - shows only sections of open limits and stop orders. 1595 :return: dictionary with client's raw portfolio and some statistics. 1596 """ 1597 if self.accountId is None or not self.accountId: 1598 uLogger.error("Variable `accountId` must be defined for using this method!") 1599 raise Exception("Account ID required") 1600 1601 view = { 1602 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1603 "headers": {}, # list of dictionaries, response headers without "positions" section 1604 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1605 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1606 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1607 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1608 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1609 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1610 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1611 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1612 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1613 }, 1614 "stat": { # --- some statistics calculated using "raw" sections: 1615 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1616 "availableRUB": 0., # available rubles (without other currencies) 1617 "blockedRUB": 0., # blocked sum in Russian Rouble 1618 "totalChangesRUB": 0., # changes for all open trades in RUB 1619 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1620 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1621 "sharesCostRUB": 0., # costs of all shares in RUB 1622 "bondsCostRUB": 0., # costs of all bonds in RUB 1623 "etfsCostRUB": 0., # costs of all etfs in RUB 1624 "futuresCostRUB": 0., # costs of all futures in RUB 1625 "Currencies": [], # list of dictionaries of all currencies statistics 1626 "Shares": [], # list of dictionaries of all shares statistics 1627 "Bonds": [], # list of dictionaries of all bonds statistics 1628 "Etfs": [], # list of dictionaries of all etfs statistics 1629 "Futures": [], # list of dictionaries of all futures statistics 1630 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1631 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1632 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1633 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1634 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1635 }, 1636 "analytics": { # --- some analytics of portfolio: 1637 "distrByAssets": {}, # portfolio distribution by assets 1638 "distrByCompanies": {}, # portfolio distribution by companies 1639 "distrBySectors": {}, # portfolio distribution by sectors 1640 "distrByCurrencies": {}, # portfolio distribution by currencies 1641 "distrByCountries": {}, # portfolio distribution by countries 1642 } 1643 } 1644 1645 details = details.lower() 1646 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1647 if details not in availableDetails: 1648 details = "full" 1649 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1650 1651 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1652 1653 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1654 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1655 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1656 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1657 1658 # save response headers without "positions" section: 1659 for key in portfolioResponse.keys(): 1660 if key != "positions": 1661 view["raw"]["headers"][key] = portfolioResponse[key] 1662 1663 else: 1664 continue 1665 1666 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1667 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1668 for item in portfolioResponse["positions"]: 1669 if item["instrumentType"] == "currency": 1670 self.figi = item["figi"] 1671 curr = self.SearchByFIGI(requestPrice=False) 1672 1673 # current price of currency in RUB: 1674 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1675 "name": curr["name"], 1676 "currentPrice": NanoToFloat( 1677 item["currentPrice"]["units"], 1678 item["currentPrice"]["nano"] 1679 ), 1680 } 1681 1682 view["raw"]["Currencies"].append(item) 1683 1684 elif item["instrumentType"] == "share": 1685 view["raw"]["Shares"].append(item) 1686 1687 elif item["instrumentType"] == "bond": 1688 view["raw"]["Bonds"].append(item) 1689 1690 elif item["instrumentType"] == "etf": 1691 view["raw"]["Etfs"].append(item) 1692 1693 elif item["instrumentType"] == "futures": 1694 view["raw"]["Futures"].append(item) 1695 1696 else: 1697 continue 1698 1699 # how many volume of currencies (by ISO currency name) are blocked: 1700 for item in view["raw"]["positions"]["blocked"]: 1701 blocked = NanoToFloat(item["units"], item["nano"]) 1702 if blocked > 0: 1703 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1704 1705 # how many volume of instruments (by FIGI) are blocked: 1706 for item in view["raw"]["positions"]["securities"]: 1707 blocked = int(item["blocked"]) 1708 if blocked > 0: 1709 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1710 1711 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1712 1713 if "rub" in allBlocked.keys(): 1714 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1715 1716 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1717 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1718 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1719 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1720 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1721 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1722 view["stat"]["portfolioCostRUB"] = sum([ 1723 view["stat"]["allCurrenciesCostRUB"], 1724 view["stat"]["sharesCostRUB"], 1725 view["stat"]["bondsCostRUB"], 1726 view["stat"]["etfsCostRUB"], 1727 view["stat"]["futuresCostRUB"], 1728 ]) 1729 1730 # --- calculating some portfolio statistics: 1731 byComp = {} # distribution by companies 1732 bySect = {} # distribution by sectors 1733 byCurr = {} # distribution by currencies (include RUB) 1734 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1735 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1736 1737 for item in portfolioResponse["positions"]: 1738 self.figi = item["figi"] 1739 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1740 1741 if instrument: 1742 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1743 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1744 1745 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1746 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1747 1748 else: 1749 blocked = 0 1750 1751 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1752 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1753 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1754 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1755 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1756 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1757 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1758 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1759 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1760 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1761 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1762 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1763 1764 statData = { 1765 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1766 "ticker": instrument["ticker"], # ticker by FIGI 1767 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1768 "volume": volume, # available volume of instrument 1769 "lots": lots, # volume in lots of instrument 1770 "direction": direction, # direction of an instrument's position: short or long 1771 "blocked": blocked, # blocked volume of currency or instrument 1772 "currentPrice": curPrice, # current instrument's price in basic asset 1773 "average": average, # current average position price 1774 "cost": cost, # current cost of all volume of instrument in basic asset 1775 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1776 "costRUB": costRUB, # cost of instrument in ruble 1777 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1778 "profit": profit, # expected profit at current moment 1779 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1780 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1781 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1782 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1783 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1784 "step": instrument["step"], # minimum price increment 1785 } 1786 1787 # adding distribution by unique countries: 1788 if statData["country"] not in byCountry.keys(): 1789 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1790 1791 else: 1792 byCountry[statData["country"]]["cost"] += costRUB 1793 byCountry[statData["country"]]["percent"] += percentCostRUB 1794 1795 if item["instrumentType"] != "currency": 1796 # adding distribution by unique companies: 1797 if statData["name"]: 1798 if statData["name"] not in byComp.keys(): 1799 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1800 1801 else: 1802 byComp[statData["name"]]["cost"] += costRUB 1803 byComp[statData["name"]]["percent"] += percentCostRUB 1804 1805 # adding distribution by unique sectors: 1806 if statData["sector"] not in bySect.keys(): 1807 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1808 1809 else: 1810 bySect[statData["sector"]]["cost"] += costRUB 1811 bySect[statData["sector"]]["percent"] += percentCostRUB 1812 1813 # adding distribution by unique currencies: 1814 if currency not in byCurr.keys(): 1815 byCurr[currency] = { 1816 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1817 "cost": costRUB, 1818 "percent": percentCostRUB 1819 } 1820 1821 else: 1822 byCurr[currency]["cost"] += costRUB 1823 byCurr[currency]["percent"] += percentCostRUB 1824 1825 # saving statistics for every instrument: 1826 if item["instrumentType"] == "currency": 1827 view["stat"]["Currencies"].append(statData) 1828 1829 # update dict with free funds for trading (total - blocked) by currencies 1830 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1831 view["stat"]["funds"][currency] = { 1832 "total": volume, 1833 "totalCostRUB": costRUB, # total volume cost in rubles 1834 "free": volume - blocked, 1835 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1836 } 1837 1838 elif item["instrumentType"] == "share": 1839 view["stat"]["Shares"].append(statData) 1840 1841 elif item["instrumentType"] == "bond": 1842 view["stat"]["Bonds"].append(statData) 1843 1844 elif item["instrumentType"] == "etf": 1845 view["stat"]["Etfs"].append(statData) 1846 1847 elif item["instrumentType"] == "Futures": 1848 view["stat"]["Futures"].append(statData) 1849 1850 else: 1851 continue 1852 1853 # total changes in Russian Ruble: 1854 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1855 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1856 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1857 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1858 view["stat"]["funds"]["rub"] = { 1859 "total": view["stat"]["availableRUB"], 1860 "totalCostRUB": view["stat"]["availableRUB"], 1861 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1862 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1863 } 1864 1865 # --- pending orders sector data: 1866 uniquePendingOrders = [] 1867 uniquePendingOrdersFIGIs = [] 1868 for item in view["raw"]["orders"]: 1869 if item["figi"] not in uniquePendingOrdersFIGIs: 1870 uniquePendingOrdersFIGIs.append(item["figi"]) 1871 uniquePendingOrders.append(item) 1872 1873 for item in uniquePendingOrders: 1874 self.figi = item["figi"] 1875 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1876 1877 if instrument: 1878 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1879 orderType = TKS_ORDER_TYPES[item["orderType"]] 1880 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1881 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1882 1883 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1884 if item["direction"] == "ORDER_DIRECTION_BUY": 1885 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1886 1887 else: 1888 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1889 1890 # requested price for order execution: 1891 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1892 1893 # necessary changes in percent to reach target from current price: 1894 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1895 1896 view["stat"]["orders"].append({ 1897 "orderID": item["orderId"], # orderId number parameter of current order 1898 "figi": item["figi"], # FIGI identification 1899 "ticker": instrument["ticker"], # ticker name by FIGI 1900 "lotsRequested": item["lotsRequested"], # requested lots value 1901 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1902 "currentPrice": lastPrice, # current instrument's price for defined action 1903 "targetPrice": target, # requested price for order execution in base currency 1904 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1905 "percentChanges": changes, # changes in percent to target from current price 1906 "currency": item["currency"], # instrument's currency name 1907 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1908 "type": orderType, # type of order from TKS_ORDER_TYPES 1909 "status": orderState, # order status from TKS_ORDER_STATES 1910 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1911 }) 1912 1913 # --- stop orders sector data: 1914 uniqueStopOrders = [] 1915 uniqueStopOrdersFIGIs = [] 1916 for item in view["raw"]["stopOrders"]: 1917 if item["figi"] not in uniqueStopOrdersFIGIs: 1918 uniqueStopOrdersFIGIs.append(item["figi"]) 1919 uniqueStopOrders.append(item) 1920 1921 for item in uniqueStopOrders: 1922 self.figi = item["figi"] 1923 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1924 1925 if instrument: 1926 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1927 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1928 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1929 1930 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1931 if "expirationTime" in item.keys(): 1932 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1933 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1934 1935 else: 1936 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1937 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1938 1939 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1940 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1941 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1942 1943 else: 1944 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1945 1946 # requested price when stop-order executed: 1947 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1948 1949 # price for limit-order, set up when stop-order executed: 1950 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1951 1952 # necessary changes in percent to reach target from current price: 1953 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1954 1955 view["stat"]["stopOrders"].append({ 1956 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1957 "figi": item["figi"], # FIGI identification 1958 "ticker": instrument["ticker"], # ticker name by FIGI 1959 "lotsRequested": item["lotsRequested"], # requested lots value 1960 "currentPrice": lastPrice, # current instrument's price for defined action 1961 "targetPrice": target, # requested price for stop-order execution in base currency 1962 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1963 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1964 "percentChanges": changes, # changes in percent to target from current price 1965 "currency": item["currency"], # instrument's currency name 1966 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1967 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1968 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1969 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1970 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1971 }) 1972 1973 # --- calculating data for analytics section: 1974 # portfolio distribution by assets: 1975 view["analytics"]["distrByAssets"] = { 1976 "Ruble": { 1977 "uniques": 1, 1978 "cost": view["stat"]["availableRUB"], 1979 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1980 }, 1981 "Currencies": { 1982 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1983 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1984 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1985 }, 1986 "Shares": { 1987 "uniques": len(view["stat"]["Shares"]), 1988 "cost": view["stat"]["sharesCostRUB"], 1989 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1990 }, 1991 "Bonds": { 1992 "uniques": len(view["stat"]["Bonds"]), 1993 "cost": view["stat"]["bondsCostRUB"], 1994 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1995 }, 1996 "Etfs": { 1997 "uniques": len(view["stat"]["Etfs"]), 1998 "cost": view["stat"]["etfsCostRUB"], 1999 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2000 }, 2001 "Futures": { 2002 "uniques": len(view["stat"]["Futures"]), 2003 "cost": view["stat"]["futuresCostRUB"], 2004 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2005 }, 2006 } 2007 2008 # portfolio distribution by companies: 2009 view["analytics"]["distrByCompanies"]["All money cash"] = { 2010 "ticker": "", 2011 "cost": view["stat"]["allCurrenciesCostRUB"], 2012 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2013 } 2014 view["analytics"]["distrByCompanies"].update(byComp) 2015 2016 # portfolio distribution by sectors: 2017 view["analytics"]["distrBySectors"]["All money cash"] = { 2018 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2019 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2020 } 2021 view["analytics"]["distrBySectors"].update(bySect) 2022 2023 # portfolio distribution by currencies: 2024 view["analytics"]["distrByCurrencies"].update(byCurr) 2025 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2026 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2027 2028 # portfolio distribution by countries: 2029 view["analytics"]["distrByCountries"].update(byCountry) 2030 2031 # --- Prepare text statistics overview in human-readable: 2032 if show: 2033 # Whatever the value `details`, header not changes: 2034 info = [ 2035 "# Client's portfolio\n\n", 2036 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2037 "* **Account ID:** [{}]\n".format(self.accountId), 2038 ] 2039 2040 if details in ["full", "positions", "digest"]: 2041 info.extend([ 2042 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2043 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2044 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2045 view["stat"]["totalChangesRUB"], 2046 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2047 view["stat"]["totalChangesPercentRUB"], 2048 ), 2049 ]) 2050 2051 if details in ["full", "positions"]: 2052 info.extend([ 2053 "## Open positions\n\n", 2054 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2055 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2056 "| Ruble | {:>31} | | | | | |\n".format( 2057 "{:.2f} ({:.2f}) rub".format( 2058 view["stat"]["availableRUB"], 2059 view["stat"]["blockedRUB"], 2060 ) 2061 ) 2062 ]) 2063 2064 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2065 return [ 2066 "| | | | | | | |\n", 2067 "| {:<27} | | | | | {:>19} | |\n".format( 2068 noTradeStr if noTradeStr else typeStr, 2069 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2070 ), 2071 ] 2072 2073 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2074 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2075 "{} [{}]".format(data["ticker"], data["figi"]), 2076 "{:.2f} ({:.2f}) {}".format( 2077 data["volume"], 2078 data["blocked"], 2079 data["currency"], 2080 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2081 data["volume"], 2082 data["blocked"], 2083 ), 2084 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2085 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2086 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2087 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2088 "{}{:.2f} {} ({}{:.2f}%)".format( 2089 "+" if data["profit"] > 0 else "", 2090 data["profit"], data["baseCurrencyName"], 2091 "+" if data["percentProfit"] > 0 else "", 2092 data["percentProfit"], 2093 ), 2094 ) 2095 2096 # --- Show currencies section: 2097 if view["stat"]["Currencies"]: 2098 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2099 for item in view["stat"]["Currencies"]: 2100 info.append(_InfoStr(item, showCurrencyName=True)) 2101 2102 else: 2103 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2104 2105 # --- Show shares section: 2106 if view["stat"]["Shares"]: 2107 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2108 2109 for item in view["stat"]["Shares"]: 2110 info.append(_InfoStr(item)) 2111 2112 else: 2113 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2114 2115 # --- Show bonds section: 2116 if view["stat"]["Bonds"]: 2117 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2118 2119 for item in view["stat"]["Bonds"]: 2120 info.append(_InfoStr(item)) 2121 2122 else: 2123 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2124 2125 # --- Show etfs section: 2126 if view["stat"]["Etfs"]: 2127 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2128 2129 for item in view["stat"]["Etfs"]: 2130 info.append(_InfoStr(item)) 2131 2132 else: 2133 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2134 2135 # --- Show futures section: 2136 if view["stat"]["Futures"]: 2137 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2138 2139 for item in view["stat"]["Futures"]: 2140 info.append(_InfoStr(item)) 2141 2142 else: 2143 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2144 2145 if details in ["full", "orders"]: 2146 # --- Show pending orders section: 2147 if view["stat"]["orders"]: 2148 info.extend([ 2149 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2150 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2151 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2152 ]) 2153 2154 for item in view["stat"]["orders"]: 2155 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2156 "{} [{}]".format(item["ticker"], item["figi"]), 2157 item["orderID"], 2158 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2159 "{} {} ({}{:.2f}%)".format( 2160 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2161 item["baseCurrencyName"], 2162 "+" if item["percentChanges"] > 0 else "", 2163 float(item["percentChanges"]), 2164 ), 2165 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2166 item["action"], 2167 item["type"], 2168 item["date"], 2169 )) 2170 2171 else: 2172 info.append("\n## Total pending limit-orders: 0\n") 2173 2174 # --- Show stop orders section: 2175 if view["stat"]["stopOrders"]: 2176 info.extend([ 2177 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2178 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2179 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2180 ]) 2181 2182 for item in view["stat"]["stopOrders"]: 2183 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2184 "{} [{}]".format(item["ticker"], item["figi"]), 2185 item["orderID"], 2186 item["lotsRequested"], 2187 "{} {} ({}{:.2f}%)".format( 2188 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2189 item["baseCurrencyName"], 2190 "+" if item["percentChanges"] > 0 else "", 2191 float(item["percentChanges"]), 2192 ), 2193 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2194 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2195 item["action"], 2196 item["type"], 2197 item["expType"], 2198 item["createDate"], 2199 item["expDate"], 2200 )) 2201 2202 else: 2203 info.append("\n## Total stop-orders: 0\n") 2204 2205 if details in ["full", "analytics"]: 2206 # -- Show analytics section: 2207 if view["stat"]["portfolioCostRUB"] > 0: 2208 info.extend([ 2209 "\n# Analytics\n" 2210 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2211 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2212 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2213 view["stat"]["totalChangesRUB"], 2214 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2215 view["stat"]["totalChangesPercentRUB"], 2216 ), 2217 "\n## Portfolio distribution by assets\n" 2218 "\n| Type | Uniques | Percent | Current cost |\n", 2219 "|------------|---------|---------|--------------------|\n", 2220 ]) 2221 2222 for key in view["analytics"]["distrByAssets"].keys(): 2223 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2224 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2225 key, 2226 view["analytics"]["distrByAssets"][key]["uniques"], 2227 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2228 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2229 )) 2230 2231 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2232 info.extend([ 2233 "\n## Portfolio distribution by companies\n" 2234 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2235 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2236 ]) 2237 2238 for company in view["analytics"]["distrByCompanies"].keys(): 2239 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2240 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2241 info.append("| {} | {:<7} | {:<18} |\n".format( 2242 "{}{}{}".format( 2243 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2244 company, 2245 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2246 ), 2247 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2248 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2249 )) 2250 2251 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2252 info.extend([ 2253 "\n## Portfolio distribution by sectors\n" 2254 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2255 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2256 ]) 2257 2258 for sector in view["analytics"]["distrBySectors"].keys(): 2259 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2260 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2261 sector, 2262 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2263 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2264 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2265 )) 2266 2267 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2268 info.extend([ 2269 "\n## Portfolio distribution by currencies\n" 2270 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2271 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2272 ]) 2273 2274 for curr in view["analytics"]["distrByCurrencies"].keys(): 2275 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2276 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2277 info.append("| {} | {:<7} | {:<18} |\n".format( 2278 "[{}] {}{}".format( 2279 curr, 2280 view["analytics"]["distrByCurrencies"][curr]["name"], 2281 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2282 ), 2283 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2284 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2285 )) 2286 2287 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2288 info.extend([ 2289 "\n## Portfolio distribution by countries\n" 2290 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2291 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2292 ]) 2293 2294 for country in view["analytics"]["distrByCountries"].keys(): 2295 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2296 nameLen = len(country) 2297 info.append("| {} | {:<7} | {:<18} |\n".format( 2298 "{}{}".format( 2299 country, 2300 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2301 ), 2302 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2303 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2304 )) 2305 2306 infoText = "".join(info) 2307 2308 uLogger.info(infoText) 2309 2310 if details == "full" and self.overviewFile: 2311 filename = self.overviewFile 2312 2313 elif details == "digest" and self.overviewDigestFile: 2314 filename = self.overviewDigestFile 2315 2316 elif details == "positions" and self.overviewPositionsFile: 2317 filename = self.overviewPositionsFile 2318 2319 elif details == "orders" and self.overviewOrdersFile: 2320 filename = self.overviewOrdersFile 2321 2322 elif details == "analytics" and self.overviewAnalyticsFile: 2323 filename = self.overviewAnalyticsFile 2324 2325 else: 2326 filename = "" 2327 2328 if filename: 2329 with open(filename, "w", encoding="UTF-8") as fH: 2330 fH.write(infoText) 2331 2332 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2333 2334 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be? You should specify one of strings:
full- shows full available information about portfolio status (by default),positions- shows only open positions,digest- show a short digest of the portfolio status,analytics- shows only the analytics section and the distribution of the portfolio by various categories,orders- shows only sections of open limits and stop orders.
Returns
dictionary with client's raw portfolio and some statistics.
2336 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2337 """ 2338 Returns history operations between two given dates for current `accountId`. 2339 If `reportFile` string is not empty then also save human-readable report. 2340 Shows some statistical data of closed positions. 2341 2342 :param start: see docstring in `GetDatesAsString()` method 2343 :param end: see docstring in `GetDatesAsString()` method 2344 :param show: if `True` then also prints all records to the console. 2345 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2346 :return: original list of dictionaries with history of deals records from API ("operations" key): 2347 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2348 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2349 """ 2350 if self.accountId is None or not self.accountId: 2351 uLogger.error("Variable `accountId` must be defined for using this method!") 2352 raise Exception("Account ID required") 2353 2354 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2355 2356 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2357 2358 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2359 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2360 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2361 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2362 customStat = {} # custom statistics in additional to responseJSON 2363 2364 # --- output report in human-readable format: 2365 if show or self.reportFile: 2366 splitLine1 = "| | | | | |\n" # Summary section 2367 splitLine2 = "| | | | | | | | |\n" # Operations section 2368 nextDay = "" 2369 2370 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2371 2372 if len(ops) > 0: 2373 customStat = { 2374 "opsCount": 0, # total operations count 2375 "buyCount": 0, # buy operations 2376 "sellCount": 0, # sell operations 2377 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2378 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2379 "payIn": {"rub": 0.}, # Deposit brokerage account 2380 "payOut": {"rub": 0.}, # Withdrawals 2381 "divs": {"rub": 0.}, # Dividends income 2382 "coupons": {"rub": 0.}, # Coupon's income 2383 "brokerCom": {"rub": 0.}, # Service commissions 2384 "serviceCom": {"rub": 0.}, # Service commissions 2385 "marginCom": {"rub": 0.}, # Margin commissions 2386 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2387 } 2388 2389 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2390 for item in ops: 2391 if item["state"] == "OPERATION_STATE_EXECUTED": 2392 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2393 2394 # count buy operations: 2395 if "_BUY" in item["operationType"]: 2396 customStat["buyCount"] += 1 2397 2398 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2399 customStat["buyTotal"][item["payment"]["currency"]] += payment 2400 2401 else: 2402 customStat["buyTotal"][item["payment"]["currency"]] = payment 2403 2404 # count sell operations: 2405 elif "_SELL" in item["operationType"]: 2406 customStat["sellCount"] += 1 2407 2408 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2409 customStat["sellTotal"][item["payment"]["currency"]] += payment 2410 2411 else: 2412 customStat["sellTotal"][item["payment"]["currency"]] = payment 2413 2414 # count incoming operations: 2415 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2416 if item["payment"]["currency"] in customStat["payIn"].keys(): 2417 customStat["payIn"][item["payment"]["currency"]] += payment 2418 2419 else: 2420 customStat["payIn"][item["payment"]["currency"]] = payment 2421 2422 # count withdrawals operations: 2423 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2424 if item["payment"]["currency"] in customStat["payOut"].keys(): 2425 customStat["payOut"][item["payment"]["currency"]] += payment 2426 2427 else: 2428 customStat["payOut"][item["payment"]["currency"]] = payment 2429 2430 # count dividends income: 2431 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2432 if item["payment"]["currency"] in customStat["divs"].keys(): 2433 customStat["divs"][item["payment"]["currency"]] += payment 2434 2435 else: 2436 customStat["divs"][item["payment"]["currency"]] = payment 2437 2438 # count coupon's income: 2439 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2440 if item["payment"]["currency"] in customStat["coupons"].keys(): 2441 customStat["coupons"][item["payment"]["currency"]] += payment 2442 2443 else: 2444 customStat["coupons"][item["payment"]["currency"]] = payment 2445 2446 # count broker commissions: 2447 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2448 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2449 customStat["brokerCom"][item["payment"]["currency"]] += payment 2450 2451 else: 2452 customStat["brokerCom"][item["payment"]["currency"]] = payment 2453 2454 # count service commissions: 2455 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2456 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2457 customStat["serviceCom"][item["payment"]["currency"]] += payment 2458 2459 else: 2460 customStat["serviceCom"][item["payment"]["currency"]] = payment 2461 2462 # count margin commissions: 2463 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2464 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2465 customStat["marginCom"][item["payment"]["currency"]] += payment 2466 2467 else: 2468 customStat["marginCom"][item["payment"]["currency"]] = payment 2469 2470 # count withholding taxes: 2471 elif "_TAX" in item["operationType"]: 2472 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2473 customStat["allTaxes"][item["payment"]["currency"]] += payment 2474 2475 else: 2476 customStat["allTaxes"][item["payment"]["currency"]] = payment 2477 2478 else: 2479 continue 2480 2481 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2482 2483 # --- view "Actions" lines: 2484 info.extend([ 2485 "| 1 | 2 | 3 | 4 | 5 |\n", 2486 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2487 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2488 "| | Buy: {:<22} | {:<28} | | |\n".format( 2489 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2490 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2491 ), 2492 "| | Sell: {:<21} | {:<28} | | |\n".format( 2493 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2494 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2495 ), 2496 ]) 2497 2498 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2499 for key in opsKeys: 2500 if key == "rub": 2501 continue 2502 2503 info.extend([ 2504 "| | | {:<28} | | |\n".format( 2505 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2506 ), 2507 "| | | {:<28} | | |\n".format( 2508 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2509 ), 2510 ]) 2511 2512 info.append(splitLine1) 2513 2514 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2515 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2516 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2517 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2518 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2519 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2520 ) 2521 2522 # --- view "Payments" lines: 2523 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2524 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2525 2526 for key in paymentsKeys: 2527 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2528 2529 info.append(splitLine1) 2530 2531 # --- view "Commissions and taxes" lines: 2532 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2533 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2534 2535 for key in comKeys: 2536 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2537 2538 info.append(splitLine1) 2539 2540 info.extend([ 2541 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2542 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2543 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2544 ]) 2545 2546 else: 2547 info.append("Broker returned no operations during this period\n") 2548 2549 # --- view "Operations" section: 2550 for item in ops: 2551 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2552 continue 2553 2554 else: 2555 self.figi = item["figi"] if item["figi"] else "" 2556 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2557 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2558 2559 # group of deals during one day: 2560 if nextDay and item["date"].split("T")[0] != nextDay: 2561 info.append(splitLine2) 2562 nextDay = "" 2563 2564 else: 2565 nextDay = item["date"].split("T")[0] # saving current day for splitting 2566 2567 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2568 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2569 self.figi if self.figi else "—", 2570 instrument["ticker"] if instrument else "—", 2571 instrument["type"] if instrument else "—", 2572 item["quantity"] if int(item["quantity"]) > 0 else "—", 2573 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2574 TKS_OPERATION_STATES[item["state"]], 2575 TKS_OPERATION_TYPES[item["operationType"]], 2576 )) 2577 2578 infoText = "".join(info) 2579 2580 if show: 2581 uLogger.info(infoText) 2582 2583 if self.reportFile: 2584 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2585 fH.write(infoText) 2586 2587 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2588 2589 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
GetDatesAsString()method - end: see docstring in
GetDatesAsString()method - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2591 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2592 """ 2593 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2594 2595 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2596 Warning! Broker server used ISO UTC time by default. 2597 2598 If `historyFile` is not `None` then method save history to file, otherwise return only pandas dataframe. 2599 Also, `historyFile` used to update history with `onlyMissing` parameter. 2600 2601 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2602 2603 :param start: see docstring in `GetDatesAsString()` method. 2604 :param end: see docstring in `GetDatesAsString()` method. 2605 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2606 `"hour"`, `"day"`. Default: `"hour"`. 2607 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2608 False by default. Warning! History appends only from last candle to current time 2609 with always update last candle! 2610 :param csvSep: separator if csv-file is used, `,` by default. 2611 :param show: if `True` then also prints pandas dataframe to the console. 2612 :return: pandas dataframe with prices history. Headers of columns are defined by default: 2613 `["date", "time", "open", "high", "low", "close", "volume"]`. 2614 """ 2615 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2616 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2617 history = None # empty pandas object for history 2618 2619 if interval not in TKS_CANDLE_INTERVALS.keys(): 2620 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2621 raise Exception("Incorrect value") 2622 2623 if not (self.ticker or self.figi): 2624 uLogger.error("Ticker or FIGI must be defined!") 2625 raise Exception("Ticker or FIGI required") 2626 2627 if self.ticker and not self.figi: 2628 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2629 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2630 2631 if self.figi and not self.ticker: 2632 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2633 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2634 2635 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2636 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2637 if interval.lower() != "day": 2638 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2639 2640 delta = dtEnd - dtStart # current UTC time minus last time in file 2641 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2642 2643 # calculate history length in candles: 2644 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2645 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2646 length += 1 # to avoid fraction time 2647 2648 # calculate data blocks count: 2649 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2650 2651 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2652 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2653 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2654 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2655 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2656 2657 tempOld = None # pandas object for old history, if --only-missing key present 2658 lastTime = None # datetime object of last old candle in file 2659 2660 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2661 uLogger.debug("--only-missing key present, add only last missing candles...") 2662 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2663 2664 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2665 2666 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2667 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2668 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2669 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2670 2671 # get last datetime object from last string in file or minus 1 delta if file is empty: 2672 if len(tempOld) > 0: 2673 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2674 2675 else: 2676 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2677 2678 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2679 2680 responseJSONs = [] # raw history blocks of data 2681 2682 blockEnd = dtEnd 2683 for item in range(blocks): 2684 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2685 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2686 2687 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2688 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2689 )) 2690 2691 if blockStart == blockEnd: 2692 uLogger.debug("Skipped this zero-length block...") 2693 2694 else: 2695 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2696 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2697 self.body = str({ 2698 "figi": self.figi, 2699 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2700 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2701 "interval": TKS_CANDLE_INTERVALS[interval][0] 2702 }) 2703 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2704 2705 if "code" in responseJSON.keys(): 2706 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2707 2708 else: 2709 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2710 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2711 2712 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2713 2714 blockEnd = blockStart 2715 2716 printCount = len(responseJSONs) # candles to show in console 2717 if responseJSONs: 2718 tempHistory = pd.DataFrame( 2719 data={ 2720 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2721 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2722 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2723 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2724 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2725 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2726 "volume": [int(item["volume"]) for item in responseJSONs], 2727 }, 2728 index=range(len(responseJSONs)), 2729 columns=["date", "time", "open", "high", "low", "close", "volume"], 2730 ) 2731 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2732 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2733 2734 # append only newest candles to old history if --only-missing key present: 2735 if onlyMissing and tempOld is not None and lastTime is not None: 2736 index = 0 # find start index in tempHistory data: 2737 2738 for i, item in tempHistory.iterrows(): 2739 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2740 2741 if curTime == lastTime: 2742 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2743 index = i 2744 printCount = index + 1 2745 break 2746 2747 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2748 2749 else: 2750 history = tempHistory # if no `--only-missing` key then load full data from server 2751 2752 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2753 2754 if history is not None and not history.empty: 2755 if show: 2756 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2757 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2758 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2759 )) 2760 2761 else: 2762 uLogger.warning("Received an empty candles history!") 2763 2764 if self.historyFile is not None: 2765 if history is not None and not history.empty: 2766 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2767 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2768 2769 else: 2770 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2771 2772 else: 2773 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only pandas dataframe returns.") 2774 2775 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only pandas dataframe.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
GetDatesAsString()method. - end: see docstring in
GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints pandas dataframe to the console.
Returns
pandas dataframe with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2777 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2778 """ 2779 Load candles history from csv-file and return pandas dataframe object. 2780 2781 See also: `History()` and `ShowHistoryChart()` methods. 2782 2783 :param filePath: path to csv-file to open. 2784 """ 2785 loadedHistory = None # init candles data object 2786 2787 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2788 2789 if os.path.exists(filePath): 2790 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as pandas dataframe 2791 2792 tfStr = self.priceModel.FormattedDelta( 2793 self.priceModel.timeframe, 2794 "{days} days {hours}h {minutes}m {seconds}s", 2795 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2796 self.priceModel.timeframe, 2797 "{hours}h {minutes}m {seconds}s", 2798 ) 2799 2800 if loadedHistory is not None and not loadedHistory.empty: 2801 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2802 len(loadedHistory), 2803 tfStr, 2804 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2805 ) 2806 2807 else: 2808 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2809 2810 else: 2811 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2812 2813 return loadedHistory
Load candles history from csv-file and return pandas dataframe object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2815 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2816 """ 2817 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2818 2819 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2820 Default: `index.html` (both for interact and non-interact candlesticks chart). 2821 2822 See also: `History()` and `LoadHistory()` methods. 2823 2824 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2825 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2826 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2827 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2828 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2829 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2830 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2831 """ 2832 if isinstance(candles, str): 2833 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2834 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2835 2836 elif isinstance(candles, pd.DataFrame): 2837 self.priceModel.prices = candles # set candles chain from variable 2838 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2839 2840 if "datetime" not in candles.columns: 2841 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2842 2843 else: 2844 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2845 raise Exception("Incorrect value") 2846 2847 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2848 2849 if interact: 2850 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2851 2852 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2853 2854 else: 2855 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2856 2857 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2858 2859 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2861 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2862 """ 2863 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2864 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2865 2866 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2867 2868 :param operation: string "Buy" or "Sell". 2869 :param lots: volume, integer count of lots >= 1. 2870 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2871 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2872 :param expDate: string "Undefined" by default or local date in future, 2873 it is a string with format `%Y-%m-%d %H:%M:%S`. 2874 :return: JSON with response from broker server. 2875 """ 2876 if self.accountId is None or not self.accountId: 2877 uLogger.error("Variable `accountId` must be defined for using this method!") 2878 raise Exception("Account ID required") 2879 2880 if operation is None or not operation or operation not in ("Buy", "Sell"): 2881 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2882 raise Exception("Incorrect value") 2883 2884 if lots is None or lots < 1: 2885 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2886 lots = 1 2887 2888 if tp is None or tp < 0: 2889 tp = 0 2890 2891 if sl is None or sl < 0: 2892 sl = 0 2893 2894 if expDate is None or not expDate: 2895 expDate = "Undefined" 2896 2897 if not (self.ticker or self.figi): 2898 uLogger.error("Ticker or FIGI must be defined!") 2899 raise Exception("Ticker or FIGI required") 2900 2901 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2902 self.ticker = instrument["ticker"] 2903 self.figi = instrument["figi"] 2904 2905 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2906 2907 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2908 self.body = str({ 2909 "figi": self.figi, 2910 "quantity": str(lots), 2911 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2912 "accountId": str(self.accountId), 2913 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2914 }) 2915 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2916 2917 if "orderId" in response.keys(): 2918 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2919 operation, response["orderId"], 2920 self.ticker, self.figi, lots, 2921 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2922 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2923 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2924 )) 2925 2926 else: 2927 uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.") 2928 2929 if tp > 0: 2930 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2931 2932 if sl > 0: 2933 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2934 2935 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2937 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2938 """ 2939 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2940 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2941 2942 See also: `Order()` and `Trade()` docstrings. 2943 2944 :param lots: volume, integer count of lots >= 1. 2945 :param tp: float > 0, take profit price of stop-order. 2946 :param sl: float > 0, stop loss price of stop-order. 2947 :param expDate: it's a local date in future. 2948 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2949 :return: JSON with response from broker server. 2950 """ 2951 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2953 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2954 """ 2955 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2956 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2957 2958 See also: `Order()` and `Trade()` docstrings. 2959 2960 :param lots: volume, integer count of lots >= 1. 2961 :param tp: float > 0, take profit price of stop-order. 2962 :param sl: float > 0, stop loss price of stop-order. 2963 :param expDate: it's a local date in the future. 2964 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2965 :return: JSON with response from broker server. 2966 """ 2967 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2969 def CloseTrades(self, tickers: list, portfolio: dict = None) -> None: 2970 """ 2971 Close position of given instruments. 2972 2973 :param tickers: tickers list of instruments that must be closed. 2974 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2975 This avoids unnecessary downloading data from the server. 2976 """ 2977 if not tickers: 2978 uLogger.info("Tickers list is empty, nothing to close.") 2979 2980 else: 2981 if portfolio is None or not portfolio: 2982 portfolio = self.Overview(show=False) 2983 2984 allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2985 uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers)) 2986 2987 for ticker in tickers: 2988 if ticker not in allOpenedTickers: 2989 uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker)) 2990 continue 2991 2992 # search open trade info about instrument by ticker: 2993 instrument = {} 2994 for iType in TKS_INSTRUMENTS: 2995 if instrument: 2996 break 2997 2998 for item in portfolio["stat"][iType]: 2999 if item["ticker"] == ticker: 3000 instrument = item 3001 break 3002 3003 if instrument: 3004 self.ticker = ticker 3005 self.figi = instrument["figi"] 3006 3007 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3008 self.ticker, 3009 self.figi, 3010 int(instrument["volume"]), 3011 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3012 )) 3013 3014 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3015 3016 if tradeLots > 0: 3017 if instrument["blocked"] > 0: 3018 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3019 instrument["blocked"], 3020 self.ticker, 3021 tradeLots, 3022 )) 3023 3024 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3025 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3026 3027 else: 3028 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
Close position of given instruments.
Parameters
- tickers: tickers list of instruments that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3030 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3031 """ 3032 Close all positions of given instruments with defined type. 3033 3034 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3035 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3036 This avoids unnecessary downloading data from the server. 3037 """ 3038 if iType not in TKS_INSTRUMENTS: 3039 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3040 3041 else: 3042 if portfolio is None or not portfolio: 3043 portfolio = self.Overview(show=False) 3044 3045 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3046 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3047 3048 if tickers and portfolio: 3049 self.CloseTrades(tickers, portfolio) 3050 3051 else: 3052 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3054 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3055 """ 3056 Universal method to create market or limit orders with all available parameters for current `accountId`. 3057 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3058 3059 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3060 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3061 3062 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3063 then broker immediately open market order as you can do simple --buy or --sell operations! 3064 3065 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3066 When current price will go up or down to target price value then broker opens a limit order. 3067 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3068 3069 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3070 3071 :param operation: string "Buy" or "Sell". 3072 :param orderType: string "Limit" or "Stop". 3073 :param lots: volume, integer count of lots >= 1. 3074 :param targetPrice: target price > 0. This is open trade price for limit order. 3075 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3076 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3077 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3078 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3079 Stop loss order always executed by market price. 3080 :param expDate: string "Undefined" by default or local date in future. 3081 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3082 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3083 A limit order has no expiration date, it lasts until the end of the trading day. 3084 :return: JSON with response from broker server. 3085 """ 3086 if self.accountId is None or not self.accountId: 3087 uLogger.error("Variable `accountId` must be defined for using this method!") 3088 raise Exception("Account ID required") 3089 3090 if operation is None or not operation or operation not in ("Buy", "Sell"): 3091 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3092 raise Exception("Incorrect value") 3093 3094 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3095 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3096 raise Exception("Incorrect value") 3097 3098 if lots is None or lots < 1: 3099 uLogger.error("You must define trade volume > 0: integer count of lots!") 3100 raise Exception("Incorrect value") 3101 3102 if targetPrice is None or targetPrice <= 0: 3103 uLogger.error("Target price for limit-order must be greater than 0!") 3104 raise Exception("Incorrect value") 3105 3106 if limitPrice is None or limitPrice <= 0: 3107 limitPrice = targetPrice 3108 3109 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3110 stopType = "Limit" 3111 3112 if expDate is None or not expDate: 3113 expDate = "Undefined" 3114 3115 if not (self.ticker or self.figi): 3116 uLogger.error("Tocker or FIGI must be defined!") 3117 raise Exception("Ticker or FIGI required") 3118 3119 response = {} 3120 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3121 self.ticker = instrument["ticker"] 3122 self.figi = instrument["figi"] 3123 3124 if orderType == "Limit": 3125 uLogger.debug( 3126 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3127 self.ticker, self.figi, 3128 operation, lots, targetPrice, instrument["currency"], 3129 )) 3130 3131 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3132 self.body = str({ 3133 "figi": self.figi, 3134 "quantity": str(lots), 3135 "price": FloatToNano(targetPrice), 3136 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3137 "accountId": str(self.accountId), 3138 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3139 }) 3140 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3141 3142 if "orderId" in response.keys(): 3143 uLogger.info( 3144 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3145 response["orderId"], 3146 self.ticker, self.figi, 3147 operation, lots, targetPrice, instrument["currency"], 3148 )) 3149 3150 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3151 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3152 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3153 targetPrice, instrument["currency"], 3154 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3155 )) 3156 3157 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3158 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3159 targetPrice, instrument["currency"], 3160 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3161 )) 3162 3163 else: 3164 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3165 3166 if orderType == "Stop": 3167 uLogger.debug( 3168 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3169 self.ticker, self.figi, 3170 operation, lots, 3171 targetPrice, instrument["currency"], 3172 limitPrice, instrument["currency"], 3173 stopType, expDate, 3174 )) 3175 3176 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3177 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3178 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3179 3180 body = { 3181 "figi": self.figi, 3182 "quantity": str(lots), 3183 "price": FloatToNano(limitPrice), 3184 "stopPrice": FloatToNano(targetPrice), 3185 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3186 "accountId": str(self.accountId), 3187 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3188 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3189 } 3190 3191 if expDateUTC: 3192 body["expireDate"] = expDateUTC 3193 3194 self.body = str(body) 3195 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3196 3197 if "stopOrderId" in response.keys(): 3198 uLogger.info( 3199 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3200 response["stopOrderId"], 3201 self.ticker, self.figi, 3202 operation, lots, 3203 targetPrice, instrument["currency"], 3204 limitPrice, instrument["currency"], 3205 TKS_STOP_ORDER_TYPES[stopOrderType], 3206 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3207 )) 3208 3209 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3210 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3211 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3212 targetPrice, instrument["currency"], 3213 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3214 )) 3215 3216 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3217 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3218 targetPrice, instrument["currency"], 3219 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3220 )) 3221 3222 else: 3223 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3224 3225 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3227 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3228 """ 3229 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3230 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3231 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3232 See also: `Order()` docstring. 3233 3234 :param lots: volume, integer count of lots >= 1. 3235 :param targetPrice: target price > 0. This is open trade price for limit order. 3236 :return: JSON with response from broker server. 3237 """ 3238 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3240 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3241 """ 3242 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3243 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3244 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3245 target price value then broker opens a limit order. See also: `Order()` docstring. 3246 3247 :param lots: volume, integer count of lots >= 1. 3248 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3249 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3250 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3251 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3252 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3253 :param expDate: string "Undefined" by default or local date in future. 3254 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3255 This date is converting to UTC format for server. 3256 :return: JSON with response from broker server. 3257 """ 3258 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3260 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3261 """ 3262 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3263 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3264 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3265 See also: `Order()` docstring. 3266 3267 :param lots: volume, integer count of lots >= 1. 3268 :param targetPrice: target price > 0. This is open trade price for limit order. 3269 :return: JSON with response from broker server. 3270 """ 3271 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3273 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3274 """ 3275 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3276 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3277 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3278 target price value then broker opens a limit order. See also: `Order()` docstring. 3279 3280 :param lots: volume, integer count of lots >= 1. 3281 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3282 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3283 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3284 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3285 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3286 :param expDate: string "Undefined" by default or local date in future. 3287 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3288 This date is converting to UTC format for server. 3289 :return: JSON with response from broker server. 3290 """ 3291 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3293 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3294 """ 3295 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3296 3297 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3298 :param allOrdersIDs: pre-received lists of all active pending orders. 3299 This avoids unnecessary downloading data from the server. 3300 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3301 """ 3302 if self.accountId is None or not self.accountId: 3303 uLogger.error("Variable `accountId` must be defined for using this method!") 3304 raise Exception("Account ID required") 3305 3306 if orderIDs: 3307 if allOrdersIDs is None or not allOrdersIDs: 3308 rawOrders = self.RequestPendingOrders() 3309 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3310 3311 if allStopOrdersIDs is None or not allStopOrdersIDs: 3312 rawStopOrders = self.RequestStopOrders() 3313 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3314 3315 for orderID in orderIDs: 3316 idInPendingOrders = orderID in allOrdersIDs 3317 idInStopOrders = orderID in allStopOrdersIDs 3318 3319 if not (idInPendingOrders or idInStopOrders): 3320 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3321 continue 3322 3323 else: 3324 if idInPendingOrders: 3325 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3326 3327 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3328 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3329 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3330 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3331 3332 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3333 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3334 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3335 3336 else: 3337 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3338 3339 elif idInStopOrders: 3340 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3341 3342 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3343 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3344 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3345 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3346 3347 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3348 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3349 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3350 3351 else: 3352 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3353 3354 else: 3355 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3357 def CloseAllOrders(self) -> None: 3358 """ 3359 Gets a list of open pending and stop orders and cancel it all. 3360 """ 3361 rawOrders = self.RequestPendingOrders() 3362 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3363 lenOrders = len(allOrdersIDs) 3364 3365 rawStopOrders = self.RequestStopOrders() 3366 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3367 lenSOrders = len(allStopOrdersIDs) 3368 3369 if lenOrders > 0 or lenSOrders > 0: 3370 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3371 3372 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3373 3374 else: 3375 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3377 def CloseAll(self, *args) -> None: 3378 """ 3379 Close all available (not blocked) opened trades and orders. 3380 3381 Also, you can select one or more keywords case-insensitive: 3382 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3383 3384 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3385 """ 3386 overview = self.Overview(show=False) # get all open trades info 3387 3388 if len(args) == 0: 3389 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3390 self.CloseAllOrders() # close all pending and stop orders 3391 3392 for iType in TKS_INSTRUMENTS: 3393 if iType != "Currencies": 3394 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3395 3396 else: 3397 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3398 lowerArgs = [x.lower() for x in args] 3399 3400 if "orders" in lowerArgs: 3401 self.CloseAllOrders() # close all pending and stop orders 3402 3403 for iType in TKS_INSTRUMENTS: 3404 if iType.lower() in lowerArgs and iType != "Currencies": 3405 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3407 @staticmethod 3408 def ParseOrderParameters(operation, **inputParameters): 3409 """ 3410 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3411 3412 :param operation: string "Buy" or "Sell". 3413 :param inputParameters: this is dict of strings that looks like this 3414 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3415 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3416 "prices" key: one or more prices to open limit-orders 3417 Counts of values in lots and prices lists must be equals! 3418 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3419 """ 3420 # TODO: update order grid work with api v2 3421 pass 3422 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3423 # 3424 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3425 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3426 # raise Exception("Incorrect value") 3427 # 3428 # if "l" in inputParameters.keys(): 3429 # inputParameters["lots"] = inputParameters.pop("l") 3430 # 3431 # if "p" in inputParameters.keys(): 3432 # inputParameters["prices"] = inputParameters.pop("p") 3433 # 3434 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3435 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3436 # raise Exception("Incorrect value") 3437 # 3438 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3439 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3440 # 3441 # if len(lots) != len(prices): 3442 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3443 # raise Exception("Incorrect value") 3444 # 3445 # uLogger.debug("Extracted parameters for orders:") 3446 # uLogger.debug("lots = {}".format(lots)) 3447 # uLogger.debug("prices = {}".format(prices)) 3448 # 3449 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3450 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3451 # uLogger.debug("Order parameters: {}".format(result)) 3452 # 3453 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3455 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3456 """ 3457 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3458 3459 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3460 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3461 """ 3462 result = False 3463 msg = "Instrument not defined!" 3464 3465 if portfolio is None or not portfolio: 3466 portfolio = self.Overview(show=False) 3467 3468 if self.ticker: 3469 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3470 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3471 3472 for iType in TKS_INSTRUMENTS: 3473 for instrument in portfolio["stat"][iType]: 3474 if instrument["ticker"] == self.ticker: 3475 result = True 3476 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3477 break 3478 3479 elif self.figi: 3480 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3481 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3482 3483 for iType in TKS_INSTRUMENTS: 3484 for instrument in portfolio["stat"][iType]: 3485 if instrument["figi"] == self.figi: 3486 result = True 3487 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3488 break 3489 3490 else: 3491 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3492 3493 uLogger.debug(msg) 3494 3495 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3497 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3498 """ 3499 Returns instrument is in the user's portfolio if it presents there. 3500 Instrument must be defined by `ticker` (highly priority) or `figi`. 3501 3502 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3503 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3504 """ 3505 result = None 3506 msg = "Instrument not defined!" 3507 3508 if portfolio is None or not portfolio: 3509 portfolio = self.Overview(show=False) 3510 3511 if self.ticker: 3512 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3513 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3514 3515 for iType in TKS_INSTRUMENTS: 3516 for instrument in portfolio["stat"][iType]: 3517 if instrument["ticker"] == self.ticker: 3518 result = instrument 3519 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3520 break 3521 3522 elif self.figi: 3523 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3524 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3525 3526 for iType in TKS_INSTRUMENTS: 3527 for instrument in portfolio["stat"][iType]: 3528 if instrument["figi"] == self.figi: 3529 result = instrument 3530 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3531 break 3532 3533 else: 3534 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3535 3536 uLogger.debug(msg) 3537 3538 return result
Returns instrument is in the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3540 def RequestLimits(self) -> dict: 3541 """ 3542 Method for obtaining the available funds for withdrawal for current `accountId`. 3543 3544 See also: 3545 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3546 - `OverviewLimits()` method 3547 3548 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3549 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3550 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3551 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3552 """ 3553 if self.accountId is None or not self.accountId: 3554 uLogger.error("Variable `accountId` must be defined for using this method!") 3555 raise Exception("Account ID required") 3556 3557 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3558 3559 self.body = str({"accountId": self.accountId}) 3560 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3561 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3562 3563 uLogger.debug("Records about available funds for withdrawal successfully received") 3564 3565 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3567 def OverviewLimits(self, show: bool = False) -> dict: 3568 """ 3569 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3570 3571 See also: `RequestLimits()`. 3572 3573 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3574 :return: dict with raw parsed data from server and some calculated statistics about it. 3575 """ 3576 if self.accountId is None or not self.accountId: 3577 uLogger.error("Variable `accountId` must be defined for using this method!") 3578 raise Exception("Account ID required") 3579 3580 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3581 3582 view = { 3583 "rawLimits": rawLimits, 3584 "limits": { # parsed data for every currency: 3585 "money": { # this is an array of portfolio currency positions 3586 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3587 }, 3588 "blocked": { # this is an array of blocked currency 3589 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3590 }, 3591 "blockedGuarantee": { # this is locked money under collateral for futures 3592 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3593 }, 3594 }, 3595 } 3596 3597 # --- Prepare text table with limits in human-readable format: 3598 if show: 3599 info = [ 3600 "# Withdrawal limits\n\n", 3601 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3602 "* **Account ID:** [{}]\n".format(self.accountId), 3603 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3604 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3605 ] 3606 3607 for curr in view["limits"]["money"].keys(): 3608 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3609 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3610 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3611 3612 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3613 "[{}]".format(curr), 3614 "{:.2f}".format(view["limits"]["money"][curr]), 3615 "{:.2f}".format(availableMoney), 3616 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3617 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3618 ) 3619 3620 if curr == "rub": 3621 info.insert(5, infoStr) # insert at first position in table and after headers 3622 3623 else: 3624 info.append(infoStr) 3625 3626 infoText = "".join(info) 3627 3628 uLogger.info(infoText) 3629 3630 if self.withdrawalLimitsFile: 3631 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3632 fH.write(infoText) 3633 3634 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3635 3636 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3638 def RequestAccounts(self) -> dict: 3639 """ 3640 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3641 3642 See also: 3643 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3644 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3645 - `OverviewUserInfo()` method 3646 3647 :return: dict with raw data from server that contains accounts info. Example of dict: 3648 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3649 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3650 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3651 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3652 """ 3653 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3654 3655 self.body = str({}) 3656 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3657 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3658 3659 uLogger.debug("Records about available accounts successfully received") 3660 3661 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
3663 def RequestUserInfo(self) -> dict: 3664 """ 3665 Method for requesting common user's information. 3666 3667 See also: 3668 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3669 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3670 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3671 - `OverviewUserInfo()` method 3672 3673 :return: dict with raw data from server that contains user's information. Example of dict: 3674 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3675 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3676 """ 3677 uLogger.debug("Requesting common user's information. Wait, please...") 3678 3679 self.body = str({}) 3680 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3681 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3682 3683 uLogger.debug("Records about current user successfully received") 3684 3685 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
3687 def RequestMarginStatus(self, accountId: str = None) -> dict: 3688 """ 3689 Method for requesting margin calculation for defined account ID. 3690 3691 See also: 3692 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3693 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3694 - `OverviewUserInfo()` method 3695 3696 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3697 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3698 Example of responses: 3699 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3700 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3701 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3702 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3703 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3704 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3705 """ 3706 if accountId is None or not accountId: 3707 if self.accountId is None or not self.accountId: 3708 uLogger.error("Variable `accountId` must be defined for using this method!") 3709 raise Exception("Account ID required") 3710 3711 else: 3712 accountId = self.accountId # use `self.accountId` (main ID) by default 3713 3714 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3715 3716 self.body = str({"accountId": accountId}) 3717 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3718 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3719 3720 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3721 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3722 rawMargin = {} 3723 3724 else: 3725 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3726 3727 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
3729 def RequestTariffLimits(self) -> dict: 3730 """ 3731 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3732 3733 See also: 3734 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3735 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3736 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3737 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3738 - `OverviewUserInfo()` method 3739 3740 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3741 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3742 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3743 """ 3744 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3745 3746 self.body = str({}) 3747 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3748 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3749 3750 uLogger.debug("Records with limits of current tariff successfully received") 3751 3752 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
3754 def RequestBondCoupons(self, iJSON: dict) -> dict: 3755 """ 3756 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3757 then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". 3758 All dates are in UTC timezone. 3759 3760 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3761 Documentation: 3762 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3763 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3764 3765 See also: `ExtendBondsData()`. 3766 3767 :param iJSON: raw json data of a bond from broker server, example: `iJSON = self.iList["Bonds"][self.ticker]` 3768 If raw iJSON is not data of bond then server returns an error [400] with message: 3769 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3770 :return: dictionary with bond payment calendar. Response example: 3771 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3772 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3773 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3774 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3775 """ 3776 if iJSON["figi"] is None or not iJSON["figi"]: 3777 uLogger.error("FIGI must be defined for using this method!") 3778 raise Exception("FIGI required") 3779 3780 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3781 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3782 3783 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3784 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3785 self.figi, 3786 startDate, 3787 endDate, 3788 )) 3789 3790 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3791 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3792 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3793 3794 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3795 uLogger.warning("Instrument type is not bond!") 3796 3797 else: 3798 uLogger.debug("Records about bond payment calendar successfully received") 3799 3800 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example:
iJSON = self.iList["Bonds"][self.ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example:
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
3802 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3803 """ 3804 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3805 pandas dataframe with more information about bonds: main info, current prices, bond payment calendar, 3806 coupon yields, current yields and some statistics etc. 3807 3808 WARNING! This is too long operation if a lot of bonds requested from broker server. 3809 3810 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3811 3812 :param instruments: list of strings with tickers or FIGIs. 3813 :param xlsx: if True then also exports pandas dataframe to xlsx-file `bondsXLSXFile`, default: `ext-bonds.xlsx`, 3814 for further used by data scientists or stock analytics. 3815 :return: wider pandas dataframe with more full and calculated data about bonds, than raw response from broker. 3816 In XLSX-file and pandas dataframe fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3817 """ 3818 if instruments is None or not instruments: 3819 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3820 raise Exception("Ticker or FIGI required") 3821 3822 if isinstance(instruments, str): 3823 instruments = [instruments] 3824 3825 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3826 3827 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3828 3829 iCount = len(uniqueInstruments) 3830 tooLong = iCount >= 20 3831 if tooLong: 3832 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3833 3834 bonds = None 3835 for i, self.figi in enumerate(uniqueInstruments): 3836 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3837 3838 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3839 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3840 rawBond = self.SearchByFIGI(requestPrice=True) 3841 3842 # Widen raw data with UTC current time (iData["actualDateTime"]): 3843 actualDate = datetime.now(tzutc()) 3844 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3845 3846 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3847 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3848 3849 # Replace some values with human-readable: 3850 iData["nominalCurrency"] = iData["nominal"]["currency"] 3851 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3852 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3853 iData["aciCurrency"] = iData["aciValue"]["currency"] 3854 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3855 iData["issueSize"] = int(iData["issueSize"]) 3856 iData["issueSizePlan"] = int(iData["issueSize"]) 3857 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3858 iData["minPriceIncrement"] = NanoToFloat(iData["minPriceIncrement"]["units"], iData["minPriceIncrement"]["nano"]) if "minPriceIncrement" in iData.keys() else 0. 3859 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3860 3861 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3862 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3863 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3864 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3865 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3866 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3867 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3868 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3869 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3870 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3871 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3872 3873 # Widen raw data with calendar data from `rawCalendar` values: 3874 calendarData = [] 3875 for item in iData["rawCalendar"]["events"]: 3876 calendarData.append({ 3877 "couponDate": item["couponDate"], 3878 "couponNumber": int(item["couponNumber"]), 3879 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3880 "payCurrency": item["payOneBond"]["currency"], 3881 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3882 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3883 "couponStartDate": item["couponStartDate"], 3884 "couponEndDate": item["couponEndDate"], 3885 "couponPeriod": item["couponPeriod"], 3886 }) 3887 3888 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3889 if "maturityDate" not in iData.keys(): 3890 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3891 3892 # Widen raw data with Coupon Rate. 3893 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3894 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3895 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3896 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3897 3898 # Widen raw data with Yield to Maturity (YTM) on current date. 3899 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3900 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3901 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3902 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3903 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3904 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3905 3906 iData["calendar"] = calendarData # adds calendar at the end 3907 3908 # Remove not used data: 3909 iData.pop("uid") 3910 iData.pop("positionUid") 3911 iData.pop("currentPrice") 3912 iData.pop("rawCalendar") 3913 3914 colNames = list(iData.keys()) 3915 if bonds is None: 3916 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3917 3918 else: 3919 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3920 3921 else: 3922 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3923 3924 processed = round(100 * (i + 1) / iCount, 1) 3925 if tooLong and processed % 5 == 0: 3926 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3927 3928 else: 3929 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3930 3931 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3932 3933 # Saving bonds from pandas dataframe to XLSX sheet: 3934 if xlsx and self.bondsXLSXFile: 3935 with pd.ExcelWriter( 3936 path=self.bondsXLSXFile, 3937 date_format=TKS_DATE_FORMAT, 3938 datetime_format=TKS_DATE_TIME_FORMAT, 3939 mode="w", 3940 ) as writer: 3941 bonds.to_excel( 3942 writer, 3943 sheet_name="Extended bonds data", 3944 index=True, 3945 encoding="UTF-8", 3946 freeze_panes=(1, 1), 3947 ) # saving as XLSX-file with freeze first row and column as headers 3948 3949 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3950 3951 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider pandas dataframe with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports pandas dataframe to xlsx-file
bondsXLSXFile, default:ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider pandas dataframe with more full and calculated data about bonds, than raw response from broker. In XLSX-file and pandas dataframe fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3953 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 3954 """ 3955 Creates bond payments calendar as pandas dataframe, and you can also save it to the XLSX-file. 3956 3957 WARNING! This is too long operation if a lot of bonds requested from broker server. 3958 3959 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 3960 3961 :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains 3962 extended information about bonds: main info, current prices, bond payment calendar, 3963 coupon yields, current yields and some statistics etc. 3964 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 3965 :param xlsx: if True then also exports pandas dataframe to file `calendarFile` + `".xlsx"`, default: `calendar.xlsx`, 3966 for further used by data scientists or stock analytics. 3967 :return: pandas dataframe with only bond payments calendar data. 3968 """ 3969 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 3970 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 3971 3972 uLogger.debug("Generating bond payments calendar data. Wait, please...") 3973 3974 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 3975 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 3976 calendar = None 3977 for bond in extBonds.iterrows(): 3978 for item in bond[1]["calendar"]: 3979 cData = { 3980 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 3981 "couponDate": item["couponDate"], 3982 "figi": bond[1]["figi"], 3983 "ticker": bond[1]["ticker"], 3984 "name": bond[1]["name"], 3985 "couponNumber": item["couponNumber"], 3986 "payOneBond": item["payOneBond"], 3987 "payCurrency": item["payCurrency"], 3988 "couponType": item["couponType"], 3989 "couponPeriod": item["couponPeriod"], 3990 "fixDate": item["fixDate"], 3991 "couponStartDate": item["couponStartDate"], 3992 "couponEndDate": item["couponEndDate"], 3993 } 3994 3995 if calendar is None: 3996 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 3997 3998 else: 3999 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4000 4001 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4002 4003 # Saving calendar from pandas dataframe to XLSX sheet: 4004 if xlsx: 4005 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4006 4007 with pd.ExcelWriter( 4008 path=xlsxCalendarFile, 4009 date_format=TKS_DATE_FORMAT, 4010 datetime_format=TKS_DATE_TIME_FORMAT, 4011 mode="w", 4012 ) as writer: 4013 humanReadable = calendar.copy(deep=True) 4014 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4015 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4016 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4017 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4018 humanReadable.columns = colNames # human-readable column names 4019 4020 humanReadable.to_excel( 4021 writer, 4022 sheet_name="Bond payments calendar", 4023 index=False, 4024 encoding="UTF-8", 4025 freeze_panes=(1, 2), 4026 ) # saving as XLSX-file with freeze first row and column as headers 4027 4028 del humanReadable # release df in memory 4029 4030 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4031 4032 return calendar
Creates bond payments calendar as pandas dataframe, and you can also save it to the XLSX-file.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: pandas dataframe object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports pandas dataframe to file
calendarFile+".xlsx", default:calendar.xlsx, for further used by data scientists or stock analytics.
Returns
pandas dataframe with only bond payments calendar data.
4034 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4035 """ 4036 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4037 4038 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 4039 4040 :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains 4041 extended information about bonds: main info, current prices, bond payment calendar, 4042 coupon yields, current yields and some statistics etc. 4043 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4044 :param show: if `True` then also printing bonds payment calendar to the console, 4045 otherwise save to file `calendarFile` only. `False` by default. 4046 :return: multilines text in Markdown format with bonds payment calendar as a table. 4047 """ 4048 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4049 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4050 4051 infoText = "# Bond payments calendar\n\n" 4052 4053 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate pandas dataframe with full calendar data 4054 4055 if not calendar.empty: 4056 splitLine = "| | | | | | | | | |\n" 4057 4058 info = [ 4059 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4060 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4061 ] 4062 4063 newMonth = False 4064 notOneBond = calendar["figi"].nunique() > 1 4065 for i, bond in enumerate(calendar.iterrows()): 4066 if newMonth and notOneBond: 4067 info.append(splitLine) 4068 4069 info.append( 4070 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4071 " +" if bond[1]["paid"] else " —", 4072 bond[1]["couponDate"].split("T")[0], 4073 bond[1]["figi"], 4074 bond[1]["ticker"], 4075 bond[1]["couponNumber"], 4076 "{} {}".format( 4077 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4078 bond[1]["payCurrency"], 4079 ), 4080 bond[1]["couponType"], 4081 bond[1]["couponPeriod"], 4082 bond[1]["fixDate"].split("T")[0], 4083 ) 4084 ) 4085 4086 if i < len(calendar.values) - 1: 4087 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4088 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4089 newMonth = False if curDate.month == nextDate.month else True 4090 4091 else: 4092 newMonth = False 4093 4094 infoText += "".join(info) 4095 4096 if show: 4097 uLogger.info("{}".format(infoText)) 4098 4099 if self.calendarFile is not None: 4100 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4101 fH.write(infoText) 4102 4103 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4104 4105 else: 4106 infoText += "No data\n" 4107 4108 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
See also: ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Parameters
- extBonds: pandas dataframe object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4110 def OverviewAccounts(self, show: bool = False) -> dict: 4111 """ 4112 Method for parsing and show simple table with all available user accounts. 4113 4114 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4115 4116 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4117 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4118 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4119 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4120 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4121 "closed": "—", "access": "Full access" }, ...}}` 4122 """ 4123 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4124 4125 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4126 accounts = { 4127 item["id"]: { 4128 "type": TKS_ACCOUNT_TYPES[item["type"]], 4129 "name": item["name"], 4130 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4131 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4132 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4133 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4134 } for item in rawAccounts["accounts"] 4135 } 4136 4137 # Raw and parsed data with some fields replaced in "stat" section: 4138 view = { 4139 "rawAccounts": rawAccounts, 4140 "stat": accounts, 4141 } 4142 4143 # --- Prepare simple text table with only accounts data in human-readable format: 4144 if show: 4145 info = [ 4146 "# User accounts\n\n", 4147 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4148 "| Account ID | Type | Status | Name |\n", 4149 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4150 ] 4151 4152 for account in view["stat"].keys(): 4153 info.extend([ 4154 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4155 account, 4156 view["stat"][account]["type"], 4157 view["stat"][account]["status"], 4158 view["stat"][account]["name"], 4159 ) 4160 ]) 4161 4162 infoText = "".join(info) 4163 4164 uLogger.info(infoText) 4165 4166 if self.userAccountsFile: 4167 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4168 fH.write(infoText) 4169 4170 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4171 4172 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4174 def OverviewUserInfo(self, show: bool = False) -> dict: 4175 """ 4176 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4177 4178 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4179 4180 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4181 :return: dict with raw parsed data from server and some calculated statistics about it. 4182 """ 4183 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4184 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4185 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4186 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4187 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4188 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4189 4190 # This is dict with parsed common user data: 4191 userInfo = { 4192 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4193 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4194 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4195 "tariff": rawUserInfo["tariff"], 4196 } 4197 4198 # This is an array of dict with parsed margin statuses for every account IDs: 4199 margins = {} 4200 for accountId in accounts.keys(): 4201 if rawMargins[accountId]: 4202 margins[accountId] = { 4203 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4204 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4205 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4206 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4207 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4208 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4209 } 4210 4211 else: 4212 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4213 4214 unary = {} # unary-connection limits 4215 for item in rawTariffLimits["unaryLimits"]: 4216 if item["limitPerMinute"] in unary.keys(): 4217 unary[item["limitPerMinute"]].extend(item["methods"]) 4218 4219 else: 4220 unary[item["limitPerMinute"]] = item["methods"] 4221 4222 stream = {} # stream-connection limits 4223 for item in rawTariffLimits["streamLimits"]: 4224 if item["limit"] in stream.keys(): 4225 stream[item["limit"]].extend(item["streams"]) 4226 4227 else: 4228 stream[item["limit"]] = item["streams"] 4229 4230 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4231 limits = { 4232 "unary": unary, 4233 "stream": stream, 4234 } 4235 4236 # Raw and parsed data as an output result: 4237 view = { 4238 "rawUserInfo": rawUserInfo, 4239 "rawAccounts": rawAccounts, 4240 "rawMargins": rawMargins, 4241 "rawTariffLimits": rawTariffLimits, 4242 "stat": { 4243 "userInfo": userInfo, 4244 "accounts": accounts, 4245 "margins": margins, 4246 "limits": limits, 4247 }, 4248 } 4249 4250 # --- Prepare text table with user information in human-readable format: 4251 if show: 4252 info = [ 4253 "# Full user information\n\n", 4254 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4255 "## Common information\n\n", 4256 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4257 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4258 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4259 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4260 "\n## User accounts\n\n", 4261 ] 4262 4263 for account in view["stat"]["accounts"].keys(): 4264 info.extend([ 4265 "### ID: [{}]\n\n".format(account), 4266 "| Parameters | Values |\n", 4267 "|----------------------|--------------------------------------------------------------|\n", 4268 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4269 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4270 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4271 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4272 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4273 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4274 ]) 4275 4276 if margins[account]: 4277 info.extend([ 4278 "| Margin status: | Enabled |\n", 4279 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4280 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4281 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4282 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4283 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4284 ]) 4285 4286 else: 4287 info.append("| Margin status: | Disabled |\n\n") 4288 4289 info.extend([ 4290 "\n## Current user tariff limits\n", 4291 "\nSee also:\n", 4292 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4293 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4294 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4295 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4296 "\n### Unary limits\n", 4297 ]) 4298 4299 if unary: 4300 for key, values in sorted(unary.items()): 4301 info.append("\n* Max requests per minute: {}\n".format(key)) 4302 4303 for value in values: 4304 info.append(" - {}\n".format(value)) 4305 4306 else: 4307 info.append("\nNot available\n") 4308 4309 info.append("\n### Stream limits\n") 4310 4311 if stream: 4312 for key, values in sorted(stream.items()): 4313 info.append("\n* Max stream connections: {}\n".format(key)) 4314 4315 for value in values: 4316 info.append(" - {}\n".format(value)) 4317 4318 else: 4319 info.append("\nNot available\n") 4320 4321 infoText = "".join(info) 4322 4323 uLogger.info(infoText) 4324 4325 if self.userInfoFile: 4326 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4327 fH.write(infoText) 4328 4329 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4330 4331 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4334class Args: 4335 """ 4336 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4337 """ 4338 def __init__(self, **kwargs): 4339 self.__dict__.update(kwargs) 4340 4341 def __getattr__(self, item): 4342 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4345def ParseArgs(): 4346 """ 4347 Function get and parse command line keys. See examples: https://tim55667757.github.io/TKSBrokerAPI/ 4348 """ 4349 parser = ArgumentParser() # command-line string parser 4350 4351 parser.description = "TKSBrokerAPI is a python API to work with some methods of Tinkoff Open API using REST protocol. It can view history, orders and market information. Also, you can open orders and trades. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples" 4352 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4353 4354 # --- options: 4355 4356 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the program. `False` by default.") 4357 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4358 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4359 4360 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4361 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4362 4363 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4364 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4365 4366 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4367 4368 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4369 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4370 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4371 4372 parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4373 4374 # --- commands: 4375 4376 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4377 4378 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4379 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4380 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider pandas dataframe with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to xlsx-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4381 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4382 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4383 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present, and also saved to file `calendar.xlsx` by default. Also, if the `--output` key present then calendar saves to file, default: `calendar.md`. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4384 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4385 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4386 4387 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4388 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4389 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4390 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4391 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4392 4393 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4394 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4395 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4396 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4397 4398 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4399 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4400 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4401 4402 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4403 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4404 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4405 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4406 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4407 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4408 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4409 4410 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4411 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4412 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.") 4413 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.") 4414 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4415 4416 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4417 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4418 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4419 4420 cmdArgs = parser.parse_args() 4421 return cmdArgs
Function get and parse command line keys. See examples: https://tim55667757.github.io/TKSBrokerAPI/
4424def Main(**kwargs): 4425 """ 4426 Main function for work with Tinkoff Open API service. It realizes simple logic: get a lot of options and execute one command. 4427 4428 See examples: https://tim55667757.github.io/TKSBrokerAPI/ 4429 """ 4430 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4431 4432 if args.debug_level: 4433 uLogger.level = 10 # always debug level by default 4434 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4435 4436 exitCode = 0 4437 start = datetime.now(tzutc()) 4438 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4439 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4440 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4441 )) 4442 4443 # trying to calculate full current version: 4444 buildVersion = __version__ 4445 try: 4446 v = version("tksbrokerapi") 4447 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4448 4449 except Exception: 4450 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4451 4452 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4453 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4454 4455 try: 4456 if args.version: 4457 print("TKSBrokerAPI {}".format(buildVersion)) 4458 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4459 4460 else: 4461 # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader` 4462 server = TinkoffBrokerServer( 4463 token=args.token, 4464 accountId=args.account_id, 4465 useCache=not args.no_cache, 4466 ) 4467 4468 # --- set some options: 4469 4470 if args.ticker: 4471 if args.ticker in server.aliasesKeys: 4472 server.ticker = server.aliases[args.ticker] # Replace some tickers with its aliases 4473 4474 else: 4475 server.ticker = args.ticker 4476 4477 if args.figi: 4478 server.figi = args.figi 4479 4480 if args.depth is not None: 4481 server.depth = args.depth 4482 4483 # --- do one of commands: 4484 4485 if args.list: 4486 if args.output is not None: 4487 server.instrumentsFile = args.output 4488 4489 server.ShowInstrumentsInfo(show=True) 4490 4491 elif args.list_xlsx: 4492 server.DumpInstrumentsAsXLSX(forceUpdate=False) 4493 4494 elif args.bonds_xlsx is not None: 4495 if args.output is not None: 4496 server.bondsXLSXFile = args.output 4497 4498 if len(args.bonds_xlsx) == 0: 4499 server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4500 4501 else: 4502 server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4503 4504 elif args.search: 4505 if args.output is not None: 4506 server.searchResultsFile = args.output 4507 4508 server.SearchInstruments(pattern=args.search[0], show=True) 4509 4510 elif args.info: 4511 if not (args.ticker or args.figi): 4512 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4513 raise Exception("Ticker or FIGI required") 4514 4515 if args.output is not None: 4516 server.infoFile = args.output 4517 4518 if args.ticker: 4519 server.SearchByTicker(requestPrice=True, show=True, debug=False) # show info and current prices by ticker name 4520 4521 else: 4522 server.SearchByFIGI(requestPrice=True, show=True, debug=False) # show info and current prices by FIGI id 4523 4524 elif args.calendar is not None: 4525 if args.output is not None: 4526 server.calendarFile = args.output 4527 4528 if len(args.calendar) == 0: 4529 bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4530 4531 else: 4532 bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4533 4534 server.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4535 4536 elif args.price: 4537 if not (args.ticker or args.figi): 4538 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4539 raise Exception("Ticker or FIGI required") 4540 4541 server.GetCurrentPrices(show=True) 4542 4543 elif args.prices is not None: 4544 if args.output is not None: 4545 server.pricesFile = args.output 4546 4547 server.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4548 4549 elif args.overview: 4550 if args.output is not None: 4551 server.overviewFile = args.output 4552 4553 server.Overview(show=True, details="full") 4554 4555 elif args.overview_digest: 4556 if args.output is not None: 4557 server.overviewDigestFile = args.output 4558 4559 server.Overview(show=True, details="digest") 4560 4561 elif args.overview_positions: 4562 if args.output is not None: 4563 server.overviewPositionsFile = args.output 4564 4565 server.Overview(show=True, details="positions") 4566 4567 elif args.overview_orders: 4568 if args.output is not None: 4569 server.overviewOrdersFile = args.output 4570 4571 server.Overview(show=True, details="orders") 4572 4573 elif args.overview_analytics: 4574 if args.output is not None: 4575 server.overviewAnalyticsFile = args.output 4576 4577 server.Overview(show=True, details="analytics") 4578 4579 elif args.deals is not None: 4580 if args.output is not None: 4581 server.reportFile = args.output 4582 4583 if 0 <= len(args.deals) < 3: 4584 server.Deals( 4585 start=args.deals[0] if len(args.deals) >= 1 else None, 4586 end=args.deals[1] if len(args.deals) == 2 else None, 4587 show=True, # Always show deals report in console 4588 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4589 ) 4590 4591 else: 4592 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4593 raise Exception("Incorrect value") 4594 4595 elif args.history is not None: 4596 if args.output is not None: 4597 server.historyFile = args.output 4598 4599 if 0 <= len(args.history) < 3: 4600 dataReceived = server.History( 4601 start=args.history[0] if len(args.history) >= 1 else None, 4602 end=args.history[1] if len(args.history) == 2 else None, 4603 interval="hour" if args.interval is None or not args.interval else args.interval, 4604 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4605 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4606 show=True, # shows all downloaded candles in console 4607 ) 4608 4609 if args.render_chart is not None and dataReceived is not None: 4610 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4611 4612 server.ShowHistoryChart( 4613 candles=dataReceived, 4614 interact=iChart, 4615 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4616 ) 4617 4618 else: 4619 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4620 raise Exception("Incorrect value") 4621 4622 elif args.load_history is not None: 4623 histData = server.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4624 4625 if args.render_chart is not None and histData is not None: 4626 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4627 server.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4628 4629 server.ShowHistoryChart( 4630 candles=histData, 4631 interact=iChart, 4632 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4633 ) 4634 4635 elif args.trade is not None: 4636 if 1 <= len(args.trade) <= 5: 4637 server.Trade( 4638 operation=args.trade[0], 4639 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4640 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4641 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4642 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4643 ) 4644 4645 else: 4646 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4647 4648 elif args.buy is not None: 4649 if 0 <= len(args.buy) <= 4: 4650 server.Buy( 4651 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4652 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4653 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4654 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4655 ) 4656 4657 else: 4658 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4659 4660 elif args.sell is not None: 4661 if 0 <= len(args.sell) <= 4: 4662 server.Sell( 4663 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4664 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4665 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4666 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4667 ) 4668 4669 else: 4670 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4671 4672 elif args.order: 4673 if 4 <= len(args.order) <= 7: 4674 server.Order( 4675 operation=args.order[0], 4676 orderType=args.order[1], 4677 lots=int(args.order[2]), 4678 targetPrice=float(args.order[3]), 4679 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4680 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4681 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4682 ) 4683 4684 else: 4685 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4686 4687 elif args.buy_limit: 4688 server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4689 4690 elif args.sell_limit: 4691 server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4692 4693 elif args.buy_stop: 4694 if 2 <= len(args.buy_stop) <= 7: 4695 server.BuyStop( 4696 lots=int(args.buy_stop[0]), 4697 targetPrice=float(args.buy_stop[1]), 4698 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4699 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4700 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4701 ) 4702 4703 else: 4704 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4705 4706 elif args.sell_stop: 4707 if 2 <= len(args.sell_stop) <= 7: 4708 server.SellStop( 4709 lots=int(args.sell_stop[0]), 4710 targetPrice=float(args.sell_stop[1]), 4711 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4712 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4713 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4714 ) 4715 4716 else: 4717 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4718 4719 # elif args.buy_order_grid is not None: 4720 # # update order grid work with api v2 4721 # if len(args.buy_order_grid) == 2: 4722 # orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4723 # 4724 # for order in orderParams: 4725 # server.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4726 # 4727 # else: 4728 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4729 # 4730 # elif args.sell_order_grid is not None: 4731 # # update order grid work with api v2 4732 # if len(args.sell_order_grid) >= 2: 4733 # orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4734 # 4735 # for order in orderParams: 4736 # server.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4737 # 4738 # else: 4739 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4740 4741 elif args.close_order is not None: 4742 server.CloseOrders(args.close_order) # close only one order 4743 4744 elif args.close_orders is not None: 4745 server.CloseOrders(args.close_orders) # close list of orders 4746 4747 elif args.close_trade: 4748 if not args.ticker: 4749 uLogger.error("`--ticker` key is required for this operation!") 4750 raise Exception("Ticker required") 4751 4752 server.CloseTrades([args.ticker]) # close only one trade 4753 4754 elif args.close_trades is not None: 4755 server.CloseTrades(args.close_trades) # close trades for list of tickers 4756 4757 elif args.close_all is not None: 4758 server.CloseAll(*args.close_all) 4759 4760 elif args.limits: 4761 if args.output is not None: 4762 server.withdrawalLimitsFile = args.output 4763 4764 server.OverviewLimits(show=True) 4765 4766 elif args.user_info: 4767 if args.output is not None: 4768 server.userInfoFile = args.output 4769 4770 server.OverviewUserInfo(show=True) 4771 4772 elif args.account: 4773 if args.output is not None: 4774 server.userAccountsFile = args.output 4775 4776 server.OverviewAccounts(show=True) 4777 4778 else: 4779 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4780 raise Exception("There is no command to execute") 4781 4782 except Exception: 4783 trace = tb.format_exc() 4784 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4785 if e in trace: 4786 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4787 break 4788 4789 uLogger.debug(trace) 4790 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4791 exitCode = 255 # an error occurred, must be open a ticket for this issue 4792 4793 finally: 4794 finish = datetime.now(tzutc()) 4795 4796 if exitCode == 0: 4797 uLogger.debug("All operations were finished success (summary code is 0).") 4798 4799 else: 4800 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4801 os.path.abspath(uLog.defaultLogFile), exitCode, 4802 )) 4803 4804 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4805 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4806 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4807 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4808 )) 4809 4810 if not kwargs: 4811 sys.exit(exitCode) 4812 4813 else: 4814 return exitCode
Main function for work with Tinkoff Open API service. It realizes simple logic: get a lot of options and execute one command.
See examples: https://tim55667757.github.io/TKSBrokerAPI/